1//! ## When to create a migration and why?
2//! A migration is necessary when keymap actions or settings are renamed or transformed (e.g., from an array to a string, a string to an array, a boolean to an enum, etc.).
3//!
4//! This ensures that users with outdated settings are automatically updated to use the corresponding new settings internally.
5//! It also provides a quick way to migrate their existing settings to the latest state using button in UI.
6//!
7//! ## How to create a migration?
8//! Migrations use Tree-sitter to query commonly used patterns, such as actions with a string or actions with an array where the second argument is an object, etc.
9//! Once queried, *you can filter out the modified items* and write the replacement logic.
10//!
11//! You *must not* modify previous migrations; always create new ones instead.
12//! This is important because if a user is in an intermediate state, they can smoothly transition to the latest state.
13//! Modifying existing migrations means they will only work for users upgrading from version x-1 to x, but not from x-2 to x, and so on, where x is the latest version.
14//!
15//! You only need to write replacement logic for x-1 to x because you can be certain that, internally, every user will be at x-1, regardless of their on disk state.
16
17use anyhow::{Context as _, Result};
18use settings_json::{infer_json_indent_size, parse_json_with_comments, update_value_in_json_text};
19use std::{cmp::Reverse, ops::Range, sync::LazyLock};
20use streaming_iterator::StreamingIterator;
21use tree_sitter::{Query, QueryMatch};
22
23use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
24
25mod migrations;
26mod patterns;
27
28fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
29 let mut parser = tree_sitter::Parser::new();
30 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
31 let syntax_tree = parser
32 .parse(text, None)
33 .context("failed to parse settings")?;
34
35 let mut cursor = tree_sitter::QueryCursor::new();
36 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
37
38 let mut edits = vec![];
39 while let Some(mat) = matches.next() {
40 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
41 edits.extend(callback(text, mat, query));
42 }
43 }
44
45 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
46 edits.dedup_by(|(range_b, _), (range_a, _)| {
47 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
48 });
49
50 if edits.is_empty() {
51 Ok(None)
52 } else {
53 let mut new_text = text.to_string();
54 for (range, replacement) in edits.iter().rev() {
55 new_text.replace_range(range.clone(), replacement);
56 }
57 if new_text == text {
58 log::error!(
59 "Edits computed for configuration migration do not cause a change: {:?}",
60 edits
61 );
62 Ok(None)
63 } else {
64 Ok(Some(new_text))
65 }
66 }
67}
68
69/// Runs the provided migrations on the given text.
70/// Will automatically return `Ok(None)` if there's no content to migrate.
71fn run_migrations(text: &str, migrations: &[MigrationType]) -> Result<Option<String>> {
72 if text.is_empty() {
73 return Ok(None);
74 }
75
76 let mut current_text = text.to_string();
77 let mut result: Option<String> = None;
78 let json_indent_size = infer_json_indent_size(¤t_text);
79 for migration in migrations.iter() {
80 let migrated_text = match migration {
81 MigrationType::TreeSitter(patterns, query) => migrate(¤t_text, patterns, query)?,
82 MigrationType::Json(callback) => {
83 if current_text.trim().is_empty() {
84 return Ok(None);
85 }
86 let old_content: serde_json_lenient::Value =
87 parse_json_with_comments(¤t_text)?;
88 let old_value = serde_json::to_value(&old_content).unwrap();
89 let mut new_value = old_value.clone();
90 callback(&mut new_value)?;
91 if new_value != old_value {
92 let mut current = current_text.clone();
93 let mut edits = vec![];
94 update_value_in_json_text(
95 &mut current,
96 &mut vec![],
97 json_indent_size,
98 &old_value,
99 &new_value,
100 &mut edits,
101 );
102 let mut migrated_text = current_text.clone();
103 for (range, replacement) in edits.into_iter() {
104 migrated_text.replace_range(range, &replacement);
105 }
106 Some(migrated_text)
107 } else {
108 None
109 }
110 }
111 };
112 if let Some(migrated_text) = migrated_text {
113 current_text = migrated_text.clone();
114 result = Some(migrated_text);
115 }
116 }
117 Ok(result.filter(|new_text| text != new_text))
118}
119
120pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
121 let migrations: &[MigrationType] = &[
122 MigrationType::TreeSitter(
123 migrations::m_2025_01_29::KEYMAP_PATTERNS,
124 &KEYMAP_QUERY_2025_01_29,
125 ),
126 MigrationType::TreeSitter(
127 migrations::m_2025_01_30::KEYMAP_PATTERNS,
128 &KEYMAP_QUERY_2025_01_30,
129 ),
130 MigrationType::TreeSitter(
131 migrations::m_2025_03_03::KEYMAP_PATTERNS,
132 &KEYMAP_QUERY_2025_03_03,
133 ),
134 MigrationType::TreeSitter(
135 migrations::m_2025_03_06::KEYMAP_PATTERNS,
136 &KEYMAP_QUERY_2025_03_06,
137 ),
138 MigrationType::TreeSitter(
139 migrations::m_2025_04_15::KEYMAP_PATTERNS,
140 &KEYMAP_QUERY_2025_04_15,
141 ),
142 MigrationType::TreeSitter(
143 migrations::m_2025_12_08::KEYMAP_PATTERNS,
144 &KEYMAP_QUERY_2025_12_08,
145 ),
146 MigrationType::TreeSitter(
147 migrations::m_2026_03_23::KEYMAP_PATTERNS,
148 &KEYMAP_QUERY_2026_03_23,
149 ),
150 ];
151 run_migrations(text, migrations)
152}
153
154enum MigrationType<'a> {
155 TreeSitter(MigrationPatterns, &'a Query),
156 Json(fn(&mut serde_json::Value) -> Result<()>),
157}
158
159pub fn migrate_settings(text: &str) -> Result<Option<String>> {
160 let migrations: &[MigrationType] = &[
161 MigrationType::TreeSitter(
162 migrations::m_2025_01_02::SETTINGS_PATTERNS,
163 &SETTINGS_QUERY_2025_01_02,
164 ),
165 MigrationType::TreeSitter(
166 migrations::m_2025_01_29::SETTINGS_PATTERNS,
167 &SETTINGS_QUERY_2025_01_29,
168 ),
169 MigrationType::TreeSitter(
170 migrations::m_2025_01_30::SETTINGS_PATTERNS,
171 &SETTINGS_QUERY_2025_01_30,
172 ),
173 MigrationType::TreeSitter(
174 migrations::m_2025_03_29::SETTINGS_PATTERNS,
175 &SETTINGS_QUERY_2025_03_29,
176 ),
177 MigrationType::TreeSitter(
178 migrations::m_2025_04_15::SETTINGS_PATTERNS,
179 &SETTINGS_QUERY_2025_04_15,
180 ),
181 MigrationType::TreeSitter(
182 migrations::m_2025_04_21::SETTINGS_PATTERNS,
183 &SETTINGS_QUERY_2025_04_21,
184 ),
185 MigrationType::TreeSitter(
186 migrations::m_2025_04_23::SETTINGS_PATTERNS,
187 &SETTINGS_QUERY_2025_04_23,
188 ),
189 MigrationType::TreeSitter(
190 migrations::m_2025_05_05::SETTINGS_PATTERNS,
191 &SETTINGS_QUERY_2025_05_05,
192 ),
193 MigrationType::TreeSitter(
194 migrations::m_2025_05_08::SETTINGS_PATTERNS,
195 &SETTINGS_QUERY_2025_05_08,
196 ),
197 MigrationType::TreeSitter(
198 migrations::m_2025_06_16::SETTINGS_PATTERNS,
199 &SETTINGS_QUERY_2025_06_16,
200 ),
201 MigrationType::TreeSitter(
202 migrations::m_2025_06_25::SETTINGS_PATTERNS,
203 &SETTINGS_QUERY_2025_06_25,
204 ),
205 MigrationType::TreeSitter(
206 migrations::m_2025_06_27::SETTINGS_PATTERNS,
207 &SETTINGS_QUERY_2025_06_27,
208 ),
209 MigrationType::TreeSitter(
210 migrations::m_2025_07_08::SETTINGS_PATTERNS,
211 &SETTINGS_QUERY_2025_07_08,
212 ),
213 MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
214 MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
215 MigrationType::TreeSitter(
216 migrations::m_2025_10_03::SETTINGS_PATTERNS,
217 &SETTINGS_QUERY_2025_10_03,
218 ),
219 MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
220 MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
221 MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
222 MigrationType::TreeSitter(
223 migrations::m_2025_11_12::SETTINGS_PATTERNS,
224 &SETTINGS_QUERY_2025_11_12,
225 ),
226 MigrationType::TreeSitter(
227 migrations::m_2025_12_01::SETTINGS_PATTERNS,
228 &SETTINGS_QUERY_2025_12_01,
229 ),
230 MigrationType::TreeSitter(
231 migrations::m_2025_11_20::SETTINGS_PATTERNS,
232 &SETTINGS_QUERY_2025_11_20,
233 ),
234 MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
235 MigrationType::TreeSitter(
236 migrations::m_2025_12_15::SETTINGS_PATTERNS,
237 &SETTINGS_QUERY_2025_12_15,
238 ),
239 MigrationType::Json(migrations::m_2025_01_27::make_auto_indent_an_enum),
240 MigrationType::Json(
241 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
242 ),
243 MigrationType::Json(migrations::m_2026_02_03::migrate_experimental_sweep_mercury),
244 MigrationType::Json(migrations::m_2026_02_04::migrate_tool_permission_defaults),
245 MigrationType::Json(migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry),
246 MigrationType::TreeSitter(
247 migrations::m_2026_03_16::SETTINGS_PATTERNS,
248 &SETTINGS_QUERY_2026_03_16,
249 ),
250 MigrationType::Json(migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum),
251 MigrationType::Json(migrations::m_2026_04_01::restructure_profiles_with_settings_key),
252 MigrationType::Json(migrations::m_2026_04_10::rename_web_search_to_search_web),
253 MigrationType::Json(
254 migrations::m_2026_04_17::promote_show_branch_icon_true_to_show_branch_status_icon,
255 ),
256 ];
257 run_migrations(text, migrations)
258}
259
260pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
261 migrate(
262 text,
263 &[(
264 SETTINGS_NESTED_KEY_VALUE_PATTERN,
265 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
266 )],
267 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
268 )
269}
270
271pub type MigrationPatterns = &'static [(
272 &'static str,
273 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
274)];
275
276macro_rules! define_query {
277 ($var_name:ident, $patterns_path:path) => {
278 static $var_name: LazyLock<Query> = LazyLock::new(|| {
279 Query::new(
280 &tree_sitter_json::LANGUAGE.into(),
281 &$patterns_path
282 .iter()
283 .map(|pattern| pattern.0)
284 .collect::<String>(),
285 )
286 .unwrap()
287 });
288 };
289}
290
291// keymap
292define_query!(
293 KEYMAP_QUERY_2025_01_29,
294 migrations::m_2025_01_29::KEYMAP_PATTERNS
295);
296define_query!(
297 KEYMAP_QUERY_2025_01_30,
298 migrations::m_2025_01_30::KEYMAP_PATTERNS
299);
300define_query!(
301 KEYMAP_QUERY_2025_03_03,
302 migrations::m_2025_03_03::KEYMAP_PATTERNS
303);
304define_query!(
305 KEYMAP_QUERY_2025_03_06,
306 migrations::m_2025_03_06::KEYMAP_PATTERNS
307);
308define_query!(
309 KEYMAP_QUERY_2025_04_15,
310 migrations::m_2025_04_15::KEYMAP_PATTERNS
311);
312
313// settings
314define_query!(
315 SETTINGS_QUERY_2025_01_02,
316 migrations::m_2025_01_02::SETTINGS_PATTERNS
317);
318define_query!(
319 SETTINGS_QUERY_2025_01_29,
320 migrations::m_2025_01_29::SETTINGS_PATTERNS
321);
322define_query!(
323 SETTINGS_QUERY_2025_01_30,
324 migrations::m_2025_01_30::SETTINGS_PATTERNS
325);
326define_query!(
327 SETTINGS_QUERY_2025_03_29,
328 migrations::m_2025_03_29::SETTINGS_PATTERNS
329);
330define_query!(
331 SETTINGS_QUERY_2025_04_15,
332 migrations::m_2025_04_15::SETTINGS_PATTERNS
333);
334define_query!(
335 SETTINGS_QUERY_2025_04_21,
336 migrations::m_2025_04_21::SETTINGS_PATTERNS
337);
338define_query!(
339 SETTINGS_QUERY_2025_04_23,
340 migrations::m_2025_04_23::SETTINGS_PATTERNS
341);
342define_query!(
343 SETTINGS_QUERY_2025_05_05,
344 migrations::m_2025_05_05::SETTINGS_PATTERNS
345);
346define_query!(
347 SETTINGS_QUERY_2025_05_08,
348 migrations::m_2025_05_08::SETTINGS_PATTERNS
349);
350define_query!(
351 SETTINGS_QUERY_2025_06_16,
352 migrations::m_2025_06_16::SETTINGS_PATTERNS
353);
354define_query!(
355 SETTINGS_QUERY_2025_06_25,
356 migrations::m_2025_06_25::SETTINGS_PATTERNS
357);
358define_query!(
359 SETTINGS_QUERY_2025_06_27,
360 migrations::m_2025_06_27::SETTINGS_PATTERNS
361);
362define_query!(
363 SETTINGS_QUERY_2025_07_08,
364 migrations::m_2025_07_08::SETTINGS_PATTERNS
365);
366define_query!(
367 SETTINGS_QUERY_2025_10_03,
368 migrations::m_2025_10_03::SETTINGS_PATTERNS
369);
370define_query!(
371 SETTINGS_QUERY_2025_11_12,
372 migrations::m_2025_11_12::SETTINGS_PATTERNS
373);
374define_query!(
375 SETTINGS_QUERY_2025_12_01,
376 migrations::m_2025_12_01::SETTINGS_PATTERNS
377);
378define_query!(
379 SETTINGS_QUERY_2025_11_20,
380 migrations::m_2025_11_20::SETTINGS_PATTERNS
381);
382define_query!(
383 KEYMAP_QUERY_2025_12_08,
384 migrations::m_2025_12_08::KEYMAP_PATTERNS
385);
386define_query!(
387 SETTINGS_QUERY_2025_12_15,
388 migrations::m_2025_12_15::SETTINGS_PATTERNS
389);
390define_query!(
391 SETTINGS_QUERY_2026_03_16,
392 migrations::m_2026_03_16::SETTINGS_PATTERNS
393);
394define_query!(
395 KEYMAP_QUERY_2026_03_23,
396 migrations::m_2026_03_23::KEYMAP_PATTERNS
397);
398
399// custom query
400static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
401 Query::new(
402 &tree_sitter_json::LANGUAGE.into(),
403 SETTINGS_NESTED_KEY_VALUE_PATTERN,
404 )
405 .unwrap()
406});
407
408#[cfg(test)]
409mod tests {
410 use super::*;
411 use unindent::Unindent as _;
412
413 #[track_caller]
414 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
415 match (&migrated, &expected) {
416 (Some(migrated), Some(expected)) => {
417 pretty_assertions::assert_str_eq!(expected, migrated);
418 }
419 _ => {
420 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
421 }
422 }
423 }
424
425 #[track_caller]
426 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
427 let migrated = migrate_keymap(input).unwrap();
428 pretty_assertions::assert_eq!(migrated.as_deref(), output);
429 }
430
431 #[track_caller]
432 fn assert_migrate_settings(input: &str, output: Option<&str>) {
433 let migrated = migrate_settings(input).unwrap();
434 assert_migrated_correctly(migrated.clone(), output);
435
436 // expect that rerunning the migration does not result in another migration
437 if let Some(migrated) = migrated {
438 let rerun = migrate_settings(&migrated).unwrap();
439 assert_migrated_correctly(rerun, None);
440 }
441 }
442
443 #[track_caller]
444 fn assert_migrate_with_migrations(
445 migrations: &[MigrationType],
446 input: &str,
447 output: Option<&str>,
448 ) {
449 let migrated = run_migrations(input, migrations).unwrap();
450 assert_migrated_correctly(migrated.clone(), output);
451
452 // expect that rerunning the migration does not result in another migration
453 if let Some(migrated) = migrated {
454 let rerun = run_migrations(&migrated, migrations).unwrap();
455 assert_migrated_correctly(rerun, None);
456 }
457 }
458
459 #[test]
460 fn test_empty_content() {
461 assert_migrate_settings("", None)
462 }
463
464 #[test]
465 fn test_replace_array_with_single_string() {
466 assert_migrate_keymap(
467 r#"
468 [
469 {
470 "bindings": {
471 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
472 }
473 }
474 ]
475 "#,
476 Some(
477 r#"
478 [
479 {
480 "bindings": {
481 "cmd-1": "workspace::ActivatePaneUp"
482 }
483 }
484 ]
485 "#,
486 ),
487 )
488 }
489
490 #[test]
491 fn test_replace_action_argument_object_with_single_value() {
492 assert_migrate_keymap(
493 r#"
494 [
495 {
496 "bindings": {
497 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
498 }
499 }
500 ]
501 "#,
502 Some(
503 r#"
504 [
505 {
506 "bindings": {
507 "cmd-1": ["editor::FoldAtLevel", 1]
508 }
509 }
510 ]
511 "#,
512 ),
513 )
514 }
515
516 #[test]
517 fn test_replace_action_argument_object_with_single_value_2() {
518 assert_migrate_keymap(
519 r#"
520 [
521 {
522 "bindings": {
523 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
524 }
525 }
526 ]
527 "#,
528 Some(
529 r#"
530 [
531 {
532 "bindings": {
533 "cmd-1": ["vim::PushObject", { "some" : "value" }]
534 }
535 }
536 ]
537 "#,
538 ),
539 )
540 }
541
542 #[test]
543 fn test_rename_string_action() {
544 assert_migrate_keymap(
545 r#"
546 [
547 {
548 "bindings": {
549 "cmd-1": "inline_completion::ToggleMenu"
550 }
551 }
552 ]
553 "#,
554 Some(
555 r#"
556 [
557 {
558 "bindings": {
559 "cmd-1": "edit_prediction::ToggleMenu"
560 }
561 }
562 ]
563 "#,
564 ),
565 )
566 }
567
568 #[test]
569 fn test_rename_context_key() {
570 assert_migrate_keymap(
571 r#"
572 [
573 {
574 "context": "Editor && inline_completion && !showing_completions"
575 }
576 ]
577 "#,
578 Some(
579 r#"
580 [
581 {
582 "context": "Editor && edit_prediction && !showing_completions"
583 }
584 ]
585 "#,
586 ),
587 )
588 }
589
590 #[test]
591 fn test_incremental_migrations() {
592 // Here string transforms to array internally. Then, that array transforms back to string.
593 assert_migrate_keymap(
594 r#"
595 [
596 {
597 "bindings": {
598 "ctrl-q": "editor::GoToHunk", // should remain same
599 "ctrl-w": "editor::GoToPrevHunk", // should rename
600 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
601 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
602 }
603 }
604 ]
605 "#,
606 Some(
607 r#"
608 [
609 {
610 "bindings": {
611 "ctrl-q": "editor::GoToHunk", // should remain same
612 "ctrl-w": "editor::GoToPreviousHunk", // should rename
613 "ctrl-q": "editor::GoToHunk", // should transform
614 "ctrl-w": "editor::GoToPreviousHunk" // should transform
615 }
616 }
617 ]
618 "#,
619 ),
620 )
621 }
622
623 #[test]
624 fn test_action_argument_snake_case() {
625 // First performs transformations, then replacements
626 assert_migrate_keymap(
627 r#"
628 [
629 {
630 "bindings": {
631 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
632 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
633 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
634 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
635 }
636 }
637 ]
638 "#,
639 Some(
640 r#"
641 [
642 {
643 "bindings": {
644 "cmd-1": ["vim::PushObject", { "around": false }],
645 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
646 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
647 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
648 }
649 }
650 ]
651 "#,
652 ),
653 )
654 }
655
656 #[test]
657 fn test_replace_setting_name() {
658 assert_migrate_settings(
659 r#"
660 {
661 "show_inline_completions_in_menu": true,
662 "show_inline_completions": true,
663 "inline_completions_disabled_in": ["string"],
664 "inline_completions": { "some" : "value" }
665 }
666 "#,
667 Some(
668 r#"
669 {
670 "show_edit_predictions_in_menu": true,
671 "show_edit_predictions": true,
672 "edit_predictions_disabled_in": ["string"],
673 "edit_predictions": { "some" : "value" }
674 }
675 "#,
676 ),
677 )
678 }
679
680 #[test]
681 fn test_nested_string_replace_for_settings() {
682 assert_migrate_settings(
683 &r#"
684 {
685 "features": {
686 "inline_completion_provider": "zed"
687 },
688 }
689 "#
690 .unindent(),
691 Some(
692 &r#"
693 {
694 "edit_predictions": {
695 "provider": "zed"
696 }
697 }
698 "#
699 .unindent(),
700 ),
701 )
702 }
703
704 #[test]
705 fn test_replace_settings_in_languages() {
706 assert_migrate_settings(
707 r#"
708 {
709 "languages": {
710 "Astro": {
711 "show_inline_completions": true
712 }
713 }
714 }
715 "#,
716 Some(
717 r#"
718 {
719 "languages": {
720 "Astro": {
721 "show_edit_predictions": true
722 }
723 }
724 }
725 "#,
726 ),
727 )
728 }
729
730 #[test]
731 fn test_replace_settings_value() {
732 assert_migrate_settings(
733 r#"
734 {
735 "scrollbar": {
736 "diagnostics": true
737 },
738 "chat_panel": {
739 "button": true
740 }
741 }
742 "#,
743 Some(
744 r#"
745 {
746 "scrollbar": {
747 "diagnostics": "all"
748 },
749 "chat_panel": {
750 "button": "always"
751 }
752 }
753 "#,
754 ),
755 )
756 }
757
758 #[test]
759 fn test_replace_settings_name_and_value() {
760 assert_migrate_settings(
761 r#"
762 {
763 "tabs": {
764 "always_show_close_button": true
765 }
766 }
767 "#,
768 Some(
769 r#"
770 {
771 "tabs": {
772 "show_close_button": "always"
773 }
774 }
775 "#,
776 ),
777 )
778 }
779
780 #[test]
781 fn test_replace_bash_with_terminal_in_profiles() {
782 assert_migrate_settings(
783 r#"
784 {
785 "assistant": {
786 "profiles": {
787 "custom": {
788 "name": "Custom",
789 "tools": {
790 "bash": true,
791 "diagnostics": true
792 }
793 }
794 }
795 }
796 }
797 "#,
798 Some(
799 r#"
800 {
801 "agent": {
802 "profiles": {
803 "custom": {
804 "name": "Custom",
805 "tools": {
806 "terminal": true,
807 "diagnostics": true
808 }
809 }
810 }
811 }
812 }
813 "#,
814 ),
815 )
816 }
817
818 #[test]
819 fn test_replace_bash_false_with_terminal_in_profiles() {
820 assert_migrate_settings(
821 r#"
822 {
823 "assistant": {
824 "profiles": {
825 "custom": {
826 "name": "Custom",
827 "tools": {
828 "bash": false,
829 "diagnostics": true
830 }
831 }
832 }
833 }
834 }
835 "#,
836 Some(
837 r#"
838 {
839 "agent": {
840 "profiles": {
841 "custom": {
842 "name": "Custom",
843 "tools": {
844 "terminal": false,
845 "diagnostics": true
846 }
847 }
848 }
849 }
850 }
851 "#,
852 ),
853 )
854 }
855
856 #[test]
857 fn test_no_bash_in_profiles() {
858 assert_migrate_settings(
859 r#"
860 {
861 "assistant": {
862 "profiles": {
863 "custom": {
864 "name": "Custom",
865 "tools": {
866 "diagnostics": true,
867 "find_path": true,
868 "read_file": true
869 }
870 }
871 }
872 }
873 }
874 "#,
875 Some(
876 r#"
877 {
878 "agent": {
879 "profiles": {
880 "custom": {
881 "name": "Custom",
882 "tools": {
883 "diagnostics": true,
884 "find_path": true,
885 "read_file": true
886 }
887 }
888 }
889 }
890 }
891 "#,
892 ),
893 )
894 }
895
896 #[test]
897 fn test_rename_path_search_to_find_path() {
898 assert_migrate_settings(
899 r#"
900 {
901 "assistant": {
902 "profiles": {
903 "default": {
904 "tools": {
905 "path_search": true,
906 "read_file": true
907 }
908 }
909 }
910 }
911 }
912 "#,
913 Some(
914 r#"
915 {
916 "agent": {
917 "profiles": {
918 "default": {
919 "tools": {
920 "find_path": true,
921 "read_file": true
922 }
923 }
924 }
925 }
926 }
927 "#,
928 ),
929 );
930 }
931
932 #[test]
933 fn test_rename_assistant() {
934 assert_migrate_settings(
935 r#"{
936 "assistant": {
937 "foo": "bar"
938 },
939 "edit_predictions": {
940 "enabled_in_assistant": false,
941 }
942 }"#,
943 Some(
944 r#"{
945 "agent": {
946 "foo": "bar"
947 },
948 "edit_predictions": {
949 "enabled_in_text_threads": false,
950 }
951 }"#,
952 ),
953 );
954 }
955
956 #[test]
957 fn test_comment_duplicated_agent() {
958 assert_migrate_settings(
959 r#"{
960 "agent": {
961 "name": "assistant-1",
962 "model": "gpt-4", // weird formatting
963 "utf8": "привіт"
964 },
965 "something": "else",
966 "agent": {
967 "name": "assistant-2",
968 "model": "gemini-pro"
969 }
970 }
971 "#,
972 Some(
973 r#"{
974 /* Duplicated key auto-commented: "agent": {
975 "name": "assistant-1",
976 "model": "gpt-4", // weird formatting
977 "utf8": "привіт"
978 }, */
979 "something": "else",
980 "agent": {
981 "name": "assistant-2",
982 "model": "gemini-pro"
983 }
984 }
985 "#,
986 ),
987 );
988 }
989
990 #[test]
991 fn test_mcp_settings_migration() {
992 assert_migrate_with_migrations(
993 &[MigrationType::TreeSitter(
994 migrations::m_2025_06_16::SETTINGS_PATTERNS,
995 &SETTINGS_QUERY_2025_06_16,
996 )],
997 r#"{
998 "context_servers": {
999 "empty_server": {},
1000 "extension_server": {
1001 "settings": {
1002 "foo": "bar"
1003 }
1004 },
1005 "custom_server": {
1006 "command": {
1007 "path": "foo",
1008 "args": ["bar"],
1009 "env": {
1010 "FOO": "BAR"
1011 }
1012 }
1013 },
1014 "invalid_server": {
1015 "command": {
1016 "path": "foo",
1017 "args": ["bar"],
1018 "env": {
1019 "FOO": "BAR"
1020 }
1021 },
1022 "settings": {
1023 "foo": "bar"
1024 }
1025 },
1026 "empty_server2": {},
1027 "extension_server2": {
1028 "foo": "bar",
1029 "settings": {
1030 "foo": "bar"
1031 },
1032 "bar": "foo"
1033 },
1034 "custom_server2": {
1035 "foo": "bar",
1036 "command": {
1037 "path": "foo",
1038 "args": ["bar"],
1039 "env": {
1040 "FOO": "BAR"
1041 }
1042 },
1043 "bar": "foo"
1044 },
1045 "invalid_server2": {
1046 "foo": "bar",
1047 "command": {
1048 "path": "foo",
1049 "args": ["bar"],
1050 "env": {
1051 "FOO": "BAR"
1052 }
1053 },
1054 "bar": "foo",
1055 "settings": {
1056 "foo": "bar"
1057 }
1058 }
1059 }
1060}"#,
1061 Some(
1062 r#"{
1063 "context_servers": {
1064 "empty_server": {
1065 "source": "extension",
1066 "settings": {}
1067 },
1068 "extension_server": {
1069 "source": "extension",
1070 "settings": {
1071 "foo": "bar"
1072 }
1073 },
1074 "custom_server": {
1075 "source": "custom",
1076 "command": {
1077 "path": "foo",
1078 "args": ["bar"],
1079 "env": {
1080 "FOO": "BAR"
1081 }
1082 }
1083 },
1084 "invalid_server": {
1085 "source": "custom",
1086 "command": {
1087 "path": "foo",
1088 "args": ["bar"],
1089 "env": {
1090 "FOO": "BAR"
1091 }
1092 },
1093 "settings": {
1094 "foo": "bar"
1095 }
1096 },
1097 "empty_server2": {
1098 "source": "extension",
1099 "settings": {}
1100 },
1101 "extension_server2": {
1102 "source": "extension",
1103 "foo": "bar",
1104 "settings": {
1105 "foo": "bar"
1106 },
1107 "bar": "foo"
1108 },
1109 "custom_server2": {
1110 "source": "custom",
1111 "foo": "bar",
1112 "command": {
1113 "path": "foo",
1114 "args": ["bar"],
1115 "env": {
1116 "FOO": "BAR"
1117 }
1118 },
1119 "bar": "foo"
1120 },
1121 "invalid_server2": {
1122 "source": "custom",
1123 "foo": "bar",
1124 "command": {
1125 "path": "foo",
1126 "args": ["bar"],
1127 "env": {
1128 "FOO": "BAR"
1129 }
1130 },
1131 "bar": "foo",
1132 "settings": {
1133 "foo": "bar"
1134 }
1135 }
1136 }
1137}"#,
1138 ),
1139 );
1140 }
1141
1142 #[test]
1143 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1144 let settings = r#"{
1145 "context_servers": {
1146 "empty_server": {
1147 "source": "extension",
1148 "settings": {}
1149 },
1150 "extension_server": {
1151 "source": "extension",
1152 "settings": {
1153 "foo": "bar"
1154 }
1155 },
1156 "custom_server": {
1157 "source": "custom",
1158 "command": {
1159 "path": "foo",
1160 "args": ["bar"],
1161 "env": {
1162 "FOO": "BAR"
1163 }
1164 }
1165 },
1166 "invalid_server": {
1167 "source": "custom",
1168 "command": {
1169 "path": "foo",
1170 "args": ["bar"],
1171 "env": {
1172 "FOO": "BAR"
1173 }
1174 },
1175 "settings": {
1176 "foo": "bar"
1177 }
1178 }
1179 }
1180}"#;
1181 assert_migrate_with_migrations(
1182 &[MigrationType::TreeSitter(
1183 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1184 &SETTINGS_QUERY_2025_06_16,
1185 )],
1186 settings,
1187 None,
1188 );
1189 }
1190
1191 #[test]
1192 fn test_custom_agent_server_settings_migration() {
1193 assert_migrate_with_migrations(
1194 &[MigrationType::TreeSitter(
1195 migrations::m_2025_11_20::SETTINGS_PATTERNS,
1196 &SETTINGS_QUERY_2025_11_20,
1197 )],
1198 r#"{
1199 "agent_servers": {
1200 "gemini": {
1201 "default_model": "gemini-1.5-pro"
1202 },
1203 "claude": {},
1204 "codex": {},
1205 "my-custom-agent": {
1206 "command": "/path/to/agent",
1207 "args": ["--foo"],
1208 "default_model": "my-model"
1209 },
1210 "already-migrated-agent": {
1211 "type": "custom",
1212 "command": "/path/to/agent"
1213 },
1214 "future-extension-agent": {
1215 "type": "extension",
1216 "default_model": "ext-model"
1217 }
1218 }
1219}"#,
1220 Some(
1221 r#"{
1222 "agent_servers": {
1223 "gemini": {
1224 "default_model": "gemini-1.5-pro"
1225 },
1226 "claude": {},
1227 "codex": {},
1228 "my-custom-agent": {
1229 "type": "custom",
1230 "command": "/path/to/agent",
1231 "args": ["--foo"],
1232 "default_model": "my-model"
1233 },
1234 "already-migrated-agent": {
1235 "type": "custom",
1236 "command": "/path/to/agent"
1237 },
1238 "future-extension-agent": {
1239 "type": "extension",
1240 "default_model": "ext-model"
1241 }
1242 }
1243}"#,
1244 ),
1245 );
1246 }
1247
1248 #[test]
1249 fn test_remove_version_fields() {
1250 assert_migrate_settings(
1251 r#"{
1252 "language_models": {
1253 "anthropic": {
1254 "version": "1",
1255 "api_url": "https://api.anthropic.com"
1256 },
1257 "openai": {
1258 "version": "1",
1259 "api_url": "https://api.openai.com/v1"
1260 }
1261 },
1262 "agent": {
1263 "version": "2",
1264 "enabled": true,
1265 "button": true,
1266 "dock": "right",
1267 "default_width": 640,
1268 "default_height": 320,
1269 "default_model": {
1270 "provider": "zed.dev",
1271 "model": "claude-sonnet-4"
1272 }
1273 }
1274}"#,
1275 Some(
1276 r#"{
1277 "language_models": {
1278 "anthropic": {
1279 "api_url": "https://api.anthropic.com"
1280 },
1281 "openai": {
1282 "api_url": "https://api.openai.com/v1"
1283 }
1284 },
1285 "agent": {
1286 "enabled": true,
1287 "button": true,
1288 "dock": "right",
1289 "default_width": 640,
1290 "default_height": 320,
1291 "default_model": {
1292 "provider": "zed.dev",
1293 "model": "claude-sonnet-4"
1294 }
1295 }
1296}"#,
1297 ),
1298 );
1299
1300 // Test that version fields in other contexts are not removed
1301 assert_migrate_settings(
1302 r#"{
1303 "language_models": {
1304 "other_provider": {
1305 "version": "1",
1306 "api_url": "https://api.example.com"
1307 }
1308 },
1309 "other_section": {
1310 "version": "1"
1311 }
1312}"#,
1313 None,
1314 );
1315 }
1316
1317 #[test]
1318 fn test_flatten_context_server_command() {
1319 assert_migrate_settings(
1320 r#"{
1321 "context_servers": {
1322 "some-mcp-server": {
1323 "command": {
1324 "path": "npx",
1325 "args": [
1326 "-y",
1327 "@supabase/mcp-server-supabase@latest",
1328 "--read-only",
1329 "--project-ref=<project-ref>"
1330 ],
1331 "env": {
1332 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1333 }
1334 }
1335 }
1336 }
1337}"#,
1338 Some(
1339 r#"{
1340 "context_servers": {
1341 "some-mcp-server": {
1342 "command": "npx",
1343 "args": [
1344 "-y",
1345 "@supabase/mcp-server-supabase@latest",
1346 "--read-only",
1347 "--project-ref=<project-ref>"
1348 ],
1349 "env": {
1350 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1351 }
1352 }
1353 }
1354}"#,
1355 ),
1356 );
1357
1358 // Test with additional keys in server object
1359 assert_migrate_settings(
1360 r#"{
1361 "context_servers": {
1362 "server-with-extras": {
1363 "command": {
1364 "path": "/usr/bin/node",
1365 "args": ["server.js"]
1366 },
1367 "settings": {}
1368 }
1369 }
1370}"#,
1371 Some(
1372 r#"{
1373 "context_servers": {
1374 "server-with-extras": {
1375 "command": "/usr/bin/node",
1376 "args": ["server.js"],
1377 "settings": {}
1378 }
1379 }
1380}"#,
1381 ),
1382 );
1383
1384 // Test command without args or env
1385 assert_migrate_settings(
1386 r#"{
1387 "context_servers": {
1388 "simple-server": {
1389 "command": {
1390 "path": "simple-mcp-server"
1391 }
1392 }
1393 }
1394}"#,
1395 Some(
1396 r#"{
1397 "context_servers": {
1398 "simple-server": {
1399 "command": "simple-mcp-server"
1400 }
1401 }
1402}"#,
1403 ),
1404 );
1405 }
1406
1407 #[test]
1408 fn test_flatten_code_action_formatters_basic_array() {
1409 assert_migrate_with_migrations(
1410 &[MigrationType::Json(
1411 migrations::m_2025_10_01::flatten_code_actions_formatters,
1412 )],
1413 &r#"{
1414 "formatter": [
1415 {
1416 "code_actions": {
1417 "included-1": true,
1418 "included-2": true,
1419 "excluded": false,
1420 }
1421 }
1422 ]
1423 }"#
1424 .unindent(),
1425 Some(
1426 &r#"{
1427 "formatter": [
1428 {
1429 "code_action": "included-1"
1430 },
1431 {
1432 "code_action": "included-2"
1433 }
1434 ]
1435 }"#
1436 .unindent(),
1437 ),
1438 );
1439 }
1440
1441 #[test]
1442 fn test_flatten_code_action_formatters_basic_object() {
1443 assert_migrate_with_migrations(
1444 &[MigrationType::Json(
1445 migrations::m_2025_10_01::flatten_code_actions_formatters,
1446 )],
1447 &r#"{
1448 "formatter": {
1449 "code_actions": {
1450 "included-1": true,
1451 "excluded": false,
1452 "included-2": true
1453 }
1454 }
1455 }"#
1456 .unindent(),
1457 Some(
1458 &r#"{
1459 "formatter": [
1460 {
1461 "code_action": "included-1"
1462 },
1463 {
1464 "code_action": "included-2"
1465 }
1466 ]
1467 }"#
1468 .unindent(),
1469 ),
1470 );
1471 }
1472
1473 #[test]
1474 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1475 assert_migrate_settings(
1476 &r#"{
1477 "formatter": [
1478 {
1479 "code_actions": {
1480 "included-1": true,
1481 "included-2": true,
1482 "excluded": false,
1483 }
1484 },
1485 {
1486 "language_server": "ruff"
1487 },
1488 {
1489 "code_actions": {
1490 "excluded": false,
1491 "excluded-2": false,
1492 }
1493 }
1494 // some comment
1495 ,
1496 {
1497 "code_actions": {
1498 "excluded": false,
1499 "included-3": true,
1500 "included-4": true,
1501 }
1502 },
1503 ]
1504 }"#
1505 .unindent(),
1506 Some(
1507 &r#"{
1508 "formatter": [
1509 {
1510 "code_action": "included-1"
1511 },
1512 {
1513 "code_action": "included-2"
1514 },
1515 {
1516 "language_server": "ruff"
1517 },
1518 {
1519 "code_action": "included-3"
1520 },
1521 {
1522 "code_action": "included-4"
1523 }
1524 ]
1525 }"#
1526 .unindent(),
1527 ),
1528 );
1529 }
1530
1531 #[test]
1532 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1533 assert_migrate_settings(
1534 &r#"{
1535 "languages": {
1536 "Rust": {
1537 "formatter": [
1538 {
1539 "code_actions": {
1540 "included-1": true,
1541 "included-2": true,
1542 "excluded": false,
1543 }
1544 },
1545 {
1546 "language_server": "ruff"
1547 },
1548 {
1549 "code_actions": {
1550 "excluded": false,
1551 "excluded-2": false,
1552 }
1553 }
1554 // some comment
1555 ,
1556 {
1557 "code_actions": {
1558 "excluded": false,
1559 "included-3": true,
1560 "included-4": true,
1561 }
1562 },
1563 ]
1564 }
1565 }
1566 }"#
1567 .unindent(),
1568 Some(
1569 &r#"{
1570 "languages": {
1571 "Rust": {
1572 "formatter": [
1573 {
1574 "code_action": "included-1"
1575 },
1576 {
1577 "code_action": "included-2"
1578 },
1579 {
1580 "language_server": "ruff"
1581 },
1582 {
1583 "code_action": "included-3"
1584 },
1585 {
1586 "code_action": "included-4"
1587 }
1588 ]
1589 }
1590 }
1591 }"#
1592 .unindent(),
1593 ),
1594 );
1595 }
1596
1597 #[test]
1598 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1599 {
1600 assert_migrate_with_migrations(
1601 &[MigrationType::Json(
1602 migrations::m_2025_10_01::flatten_code_actions_formatters,
1603 )],
1604 &r#"{
1605 "formatter": {
1606 "code_actions": {
1607 "default-1": true,
1608 "default-2": true,
1609 "default-3": true,
1610 "default-4": true,
1611 }
1612 },
1613 "languages": {
1614 "Rust": {
1615 "formatter": [
1616 {
1617 "code_actions": {
1618 "included-1": true,
1619 "included-2": true,
1620 "excluded": false,
1621 }
1622 },
1623 {
1624 "language_server": "ruff"
1625 },
1626 {
1627 "code_actions": {
1628 "excluded": false,
1629 "excluded-2": false,
1630 }
1631 }
1632 // some comment
1633 ,
1634 {
1635 "code_actions": {
1636 "excluded": false,
1637 "included-3": true,
1638 "included-4": true,
1639 }
1640 },
1641 ]
1642 },
1643 "Python": {
1644 "formatter": [
1645 {
1646 "language_server": "ruff"
1647 },
1648 {
1649 "code_actions": {
1650 "excluded": false,
1651 "excluded-2": false,
1652 }
1653 }
1654 // some comment
1655 ,
1656 {
1657 "code_actions": {
1658 "excluded": false,
1659 "included-3": true,
1660 "included-4": true,
1661 }
1662 },
1663 ]
1664 }
1665 }
1666 }"#
1667 .unindent(),
1668 Some(
1669 &r#"{
1670 "formatter": [
1671 {
1672 "code_action": "default-1"
1673 },
1674 {
1675 "code_action": "default-2"
1676 },
1677 {
1678 "code_action": "default-3"
1679 },
1680 {
1681 "code_action": "default-4"
1682 }
1683 ],
1684 "languages": {
1685 "Rust": {
1686 "formatter": [
1687 {
1688 "code_action": "included-1"
1689 },
1690 {
1691 "code_action": "included-2"
1692 },
1693 {
1694 "language_server": "ruff"
1695 },
1696 {
1697 "code_action": "included-3"
1698 },
1699 {
1700 "code_action": "included-4"
1701 }
1702 ]
1703 },
1704 "Python": {
1705 "formatter": [
1706 {
1707 "language_server": "ruff"
1708 },
1709 {
1710 "code_action": "included-3"
1711 },
1712 {
1713 "code_action": "included-4"
1714 }
1715 ]
1716 }
1717 }
1718 }"#
1719 .unindent(),
1720 ),
1721 );
1722 }
1723
1724 #[test]
1725 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1726 assert_migrate_with_migrations(
1727 &[MigrationType::Json(
1728 migrations::m_2025_10_01::flatten_code_actions_formatters,
1729 )],
1730 &r#"{
1731 "formatter": {
1732 "code_actions": {
1733 "default-1": true,
1734 "default-2": true,
1735 "default-3": true,
1736 "default-4": true,
1737 }
1738 },
1739 "format_on_save": [
1740 {
1741 "code_actions": {
1742 "included-1": true,
1743 "included-2": true,
1744 "excluded": false,
1745 }
1746 },
1747 {
1748 "language_server": "ruff"
1749 },
1750 {
1751 "code_actions": {
1752 "excluded": false,
1753 "excluded-2": false,
1754 }
1755 }
1756 // some comment
1757 ,
1758 {
1759 "code_actions": {
1760 "excluded": false,
1761 "included-3": true,
1762 "included-4": true,
1763 }
1764 },
1765 ],
1766 "languages": {
1767 "Rust": {
1768 "format_on_save": "prettier",
1769 "formatter": [
1770 {
1771 "code_actions": {
1772 "included-1": true,
1773 "included-2": true,
1774 "excluded": false,
1775 }
1776 },
1777 {
1778 "language_server": "ruff"
1779 },
1780 {
1781 "code_actions": {
1782 "excluded": false,
1783 "excluded-2": false,
1784 }
1785 }
1786 // some comment
1787 ,
1788 {
1789 "code_actions": {
1790 "excluded": false,
1791 "included-3": true,
1792 "included-4": true,
1793 }
1794 },
1795 ]
1796 },
1797 "Python": {
1798 "format_on_save": {
1799 "code_actions": {
1800 "on-save-1": true,
1801 "on-save-2": true,
1802 }
1803 },
1804 "formatter": [
1805 {
1806 "language_server": "ruff"
1807 },
1808 {
1809 "code_actions": {
1810 "excluded": false,
1811 "excluded-2": false,
1812 }
1813 }
1814 // some comment
1815 ,
1816 {
1817 "code_actions": {
1818 "excluded": false,
1819 "included-3": true,
1820 "included-4": true,
1821 }
1822 },
1823 ]
1824 }
1825 }
1826 }"#
1827 .unindent(),
1828 Some(
1829 &r#"
1830 {
1831 "formatter": [
1832 {
1833 "code_action": "default-1"
1834 },
1835 {
1836 "code_action": "default-2"
1837 },
1838 {
1839 "code_action": "default-3"
1840 },
1841 {
1842 "code_action": "default-4"
1843 }
1844 ],
1845 "format_on_save": [
1846 {
1847 "code_action": "included-1"
1848 },
1849 {
1850 "code_action": "included-2"
1851 },
1852 {
1853 "language_server": "ruff"
1854 },
1855 {
1856 "code_action": "included-3"
1857 },
1858 {
1859 "code_action": "included-4"
1860 }
1861 ],
1862 "languages": {
1863 "Rust": {
1864 "format_on_save": "prettier",
1865 "formatter": [
1866 {
1867 "code_action": "included-1"
1868 },
1869 {
1870 "code_action": "included-2"
1871 },
1872 {
1873 "language_server": "ruff"
1874 },
1875 {
1876 "code_action": "included-3"
1877 },
1878 {
1879 "code_action": "included-4"
1880 }
1881 ]
1882 },
1883 "Python": {
1884 "format_on_save": [
1885 {
1886 "code_action": "on-save-1"
1887 },
1888 {
1889 "code_action": "on-save-2"
1890 }
1891 ],
1892 "formatter": [
1893 {
1894 "language_server": "ruff"
1895 },
1896 {
1897 "code_action": "included-3"
1898 },
1899 {
1900 "code_action": "included-4"
1901 }
1902 ]
1903 }
1904 }
1905 }"#
1906 .unindent(),
1907 ),
1908 );
1909 }
1910
1911 #[test]
1912 fn test_format_on_save_formatter_migration_basic() {
1913 assert_migrate_with_migrations(
1914 &[MigrationType::Json(
1915 migrations::m_2025_10_02::remove_formatters_on_save,
1916 )],
1917 &r#"{
1918 "format_on_save": "prettier"
1919 }"#
1920 .unindent(),
1921 Some(
1922 &r#"{
1923 "formatter": "prettier",
1924 "format_on_save": "on"
1925 }"#
1926 .unindent(),
1927 ),
1928 );
1929 }
1930
1931 #[test]
1932 fn test_format_on_save_formatter_migration_array() {
1933 assert_migrate_with_migrations(
1934 &[MigrationType::Json(
1935 migrations::m_2025_10_02::remove_formatters_on_save,
1936 )],
1937 &r#"{
1938 "format_on_save": ["prettier", {"language_server": "eslint"}]
1939 }"#
1940 .unindent(),
1941 Some(
1942 &r#"{
1943 "formatter": [
1944 "prettier",
1945 {
1946 "language_server": "eslint"
1947 }
1948 ],
1949 "format_on_save": "on"
1950 }"#
1951 .unindent(),
1952 ),
1953 );
1954 }
1955
1956 #[test]
1957 fn test_format_on_save_on_off_unchanged() {
1958 assert_migrate_with_migrations(
1959 &[MigrationType::Json(
1960 migrations::m_2025_10_02::remove_formatters_on_save,
1961 )],
1962 &r#"{
1963 "format_on_save": "on"
1964 }"#
1965 .unindent(),
1966 None,
1967 );
1968
1969 assert_migrate_with_migrations(
1970 &[MigrationType::Json(
1971 migrations::m_2025_10_02::remove_formatters_on_save,
1972 )],
1973 &r#"{
1974 "format_on_save": "off"
1975 }"#
1976 .unindent(),
1977 None,
1978 );
1979 }
1980
1981 #[test]
1982 fn test_format_on_save_formatter_migration_in_languages() {
1983 assert_migrate_with_migrations(
1984 &[MigrationType::Json(
1985 migrations::m_2025_10_02::remove_formatters_on_save,
1986 )],
1987 &r#"{
1988 "languages": {
1989 "Rust": {
1990 "format_on_save": "rust-analyzer"
1991 },
1992 "Python": {
1993 "format_on_save": ["ruff", "black"]
1994 }
1995 }
1996 }"#
1997 .unindent(),
1998 Some(
1999 &r#"{
2000 "languages": {
2001 "Rust": {
2002 "formatter": "rust-analyzer",
2003 "format_on_save": "on"
2004 },
2005 "Python": {
2006 "formatter": [
2007 "ruff",
2008 "black"
2009 ],
2010 "format_on_save": "on"
2011 }
2012 }
2013 }"#
2014 .unindent(),
2015 ),
2016 );
2017 }
2018
2019 #[test]
2020 fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
2021 assert_migrate_with_migrations(
2022 &[MigrationType::Json(
2023 migrations::m_2025_10_02::remove_formatters_on_save,
2024 )],
2025 &r#"{
2026 "format_on_save": "prettier",
2027 "languages": {
2028 "Rust": {
2029 "format_on_save": "rust-analyzer"
2030 },
2031 "Python": {
2032 "format_on_save": "on"
2033 }
2034 }
2035 }"#
2036 .unindent(),
2037 Some(
2038 &r#"{
2039 "formatter": "prettier",
2040 "format_on_save": "on",
2041 "languages": {
2042 "Rust": {
2043 "formatter": "rust-analyzer",
2044 "format_on_save": "on"
2045 },
2046 "Python": {
2047 "format_on_save": "on"
2048 }
2049 }
2050 }"#
2051 .unindent(),
2052 ),
2053 );
2054 }
2055
2056 #[test]
2057 fn test_format_on_save_no_migration_when_no_format_on_save() {
2058 assert_migrate_with_migrations(
2059 &[MigrationType::Json(
2060 migrations::m_2025_10_02::remove_formatters_on_save,
2061 )],
2062 &r#"{
2063 "formatter": ["prettier"]
2064 }"#
2065 .unindent(),
2066 None,
2067 );
2068 }
2069
2070 #[test]
2071 fn test_restore_code_actions_on_format() {
2072 assert_migrate_with_migrations(
2073 &[MigrationType::Json(
2074 migrations::m_2025_10_16::restore_code_actions_on_format,
2075 )],
2076 &r#"{
2077 "formatter": {
2078 "code_action": "foo"
2079 }
2080 }"#
2081 .unindent(),
2082 Some(
2083 &r#"{
2084 "code_actions_on_format": {
2085 "foo": true
2086 },
2087 "formatter": []
2088 }"#
2089 .unindent(),
2090 ),
2091 );
2092
2093 assert_migrate_with_migrations(
2094 &[MigrationType::Json(
2095 migrations::m_2025_10_16::restore_code_actions_on_format,
2096 )],
2097 &r#"{
2098 "formatter": [
2099 { "code_action": "foo" },
2100 "auto"
2101 ]
2102 }"#
2103 .unindent(),
2104 None,
2105 );
2106
2107 assert_migrate_with_migrations(
2108 &[MigrationType::Json(
2109 migrations::m_2025_10_16::restore_code_actions_on_format,
2110 )],
2111 &r#"{
2112 "formatter": {
2113 "code_action": "foo"
2114 },
2115 "code_actions_on_format": {
2116 "bar": true,
2117 "baz": false
2118 }
2119 }"#
2120 .unindent(),
2121 Some(
2122 &r#"{
2123 "formatter": [],
2124 "code_actions_on_format": {
2125 "foo": true,
2126 "bar": true,
2127 "baz": false
2128 }
2129 }"#
2130 .unindent(),
2131 ),
2132 );
2133
2134 assert_migrate_with_migrations(
2135 &[MigrationType::Json(
2136 migrations::m_2025_10_16::restore_code_actions_on_format,
2137 )],
2138 &r#"{
2139 "formatter": [
2140 { "code_action": "foo" },
2141 { "code_action": "qux" },
2142 ],
2143 "code_actions_on_format": {
2144 "bar": true,
2145 "baz": false
2146 }
2147 }"#
2148 .unindent(),
2149 Some(
2150 &r#"{
2151 "formatter": [],
2152 "code_actions_on_format": {
2153 "foo": true,
2154 "qux": true,
2155 "bar": true,
2156 "baz": false
2157 }
2158 }"#
2159 .unindent(),
2160 ),
2161 );
2162
2163 assert_migrate_with_migrations(
2164 &[MigrationType::Json(
2165 migrations::m_2025_10_16::restore_code_actions_on_format,
2166 )],
2167 &r#"{
2168 "formatter": [],
2169 "code_actions_on_format": {
2170 "bar": true,
2171 "baz": false
2172 }
2173 }"#
2174 .unindent(),
2175 None,
2176 );
2177 }
2178
2179 #[test]
2180 fn test_make_file_finder_include_ignored_an_enum() {
2181 assert_migrate_with_migrations(
2182 &[MigrationType::Json(
2183 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2184 )],
2185 &r#"{ }"#.unindent(),
2186 None,
2187 );
2188
2189 assert_migrate_with_migrations(
2190 &[MigrationType::Json(
2191 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2192 )],
2193 &r#"{
2194 "file_finder": {
2195 "include_ignored": true
2196 }
2197 }"#
2198 .unindent(),
2199 Some(
2200 &r#"{
2201 "file_finder": {
2202 "include_ignored": "all"
2203 }
2204 }"#
2205 .unindent(),
2206 ),
2207 );
2208
2209 assert_migrate_with_migrations(
2210 &[MigrationType::Json(
2211 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2212 )],
2213 &r#"{
2214 "file_finder": {
2215 "include_ignored": false
2216 }
2217 }"#
2218 .unindent(),
2219 Some(
2220 &r#"{
2221 "file_finder": {
2222 "include_ignored": "indexed"
2223 }
2224 }"#
2225 .unindent(),
2226 ),
2227 );
2228
2229 assert_migrate_with_migrations(
2230 &[MigrationType::Json(
2231 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2232 )],
2233 &r#"{
2234 "file_finder": {
2235 "include_ignored": null
2236 }
2237 }"#
2238 .unindent(),
2239 Some(
2240 &r#"{
2241 "file_finder": {
2242 "include_ignored": "smart"
2243 }
2244 }"#
2245 .unindent(),
2246 ),
2247 );
2248
2249 // Platform key: settings nested inside "linux" should be migrated
2250 assert_migrate_with_migrations(
2251 &[MigrationType::Json(
2252 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2253 )],
2254 &r#"
2255 {
2256 "linux": {
2257 "file_finder": {
2258 "include_ignored": true
2259 }
2260 }
2261 }
2262 "#
2263 .unindent(),
2264 Some(
2265 &r#"
2266 {
2267 "linux": {
2268 "file_finder": {
2269 "include_ignored": "all"
2270 }
2271 }
2272 }
2273 "#
2274 .unindent(),
2275 ),
2276 );
2277
2278 // Profile: settings nested inside profiles should be migrated
2279 assert_migrate_with_migrations(
2280 &[MigrationType::Json(
2281 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2282 )],
2283 &r#"
2284 {
2285 "profiles": {
2286 "work": {
2287 "file_finder": {
2288 "include_ignored": false
2289 }
2290 }
2291 }
2292 }
2293 "#
2294 .unindent(),
2295 Some(
2296 &r#"
2297 {
2298 "profiles": {
2299 "work": {
2300 "file_finder": {
2301 "include_ignored": "indexed"
2302 }
2303 }
2304 }
2305 }
2306 "#
2307 .unindent(),
2308 ),
2309 );
2310 }
2311
2312 #[test]
2313 fn test_make_relative_line_numbers_an_enum() {
2314 assert_migrate_with_migrations(
2315 &[MigrationType::Json(
2316 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2317 )],
2318 &r#"{ }"#.unindent(),
2319 None,
2320 );
2321
2322 assert_migrate_with_migrations(
2323 &[MigrationType::Json(
2324 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2325 )],
2326 &r#"{
2327 "relative_line_numbers": true
2328 }"#
2329 .unindent(),
2330 Some(
2331 &r#"{
2332 "relative_line_numbers": "enabled"
2333 }"#
2334 .unindent(),
2335 ),
2336 );
2337
2338 assert_migrate_with_migrations(
2339 &[MigrationType::Json(
2340 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2341 )],
2342 &r#"{
2343 "relative_line_numbers": false
2344 }"#
2345 .unindent(),
2346 Some(
2347 &r#"{
2348 "relative_line_numbers": "disabled"
2349 }"#
2350 .unindent(),
2351 ),
2352 );
2353
2354 // Platform key: settings nested inside "macos" should be migrated
2355 assert_migrate_with_migrations(
2356 &[MigrationType::Json(
2357 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2358 )],
2359 &r#"
2360 {
2361 "macos": {
2362 "relative_line_numbers": true
2363 }
2364 }
2365 "#
2366 .unindent(),
2367 Some(
2368 &r#"
2369 {
2370 "macos": {
2371 "relative_line_numbers": "enabled"
2372 }
2373 }
2374 "#
2375 .unindent(),
2376 ),
2377 );
2378
2379 // Profile: settings nested inside profiles should be migrated
2380 assert_migrate_with_migrations(
2381 &[MigrationType::Json(
2382 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2383 )],
2384 &r#"
2385 {
2386 "profiles": {
2387 "dev": {
2388 "relative_line_numbers": false
2389 }
2390 }
2391 }
2392 "#
2393 .unindent(),
2394 Some(
2395 &r#"
2396 {
2397 "profiles": {
2398 "dev": {
2399 "relative_line_numbers": "disabled"
2400 }
2401 }
2402 }
2403 "#
2404 .unindent(),
2405 ),
2406 );
2407 }
2408
2409 #[test]
2410 fn test_make_play_sound_when_agent_done_an_enum() {
2411 assert_migrate_with_migrations(
2412 &[MigrationType::Json(
2413 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2414 )],
2415 &r#"{ }"#.unindent(),
2416 None,
2417 );
2418
2419 assert_migrate_with_migrations(
2420 &[MigrationType::Json(
2421 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2422 )],
2423 &r#"{
2424 "agent": {
2425 "play_sound_when_agent_done": true
2426 }
2427 }"#
2428 .unindent(),
2429 Some(
2430 &r#"{
2431 "agent": {
2432 "play_sound_when_agent_done": "always"
2433 }
2434 }"#
2435 .unindent(),
2436 ),
2437 );
2438
2439 assert_migrate_with_migrations(
2440 &[MigrationType::Json(
2441 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2442 )],
2443 &r#"{
2444 "agent": {
2445 "play_sound_when_agent_done": false
2446 }
2447 }"#
2448 .unindent(),
2449 Some(
2450 &r#"{
2451 "agent": {
2452 "play_sound_when_agent_done": "never"
2453 }
2454 }"#
2455 .unindent(),
2456 ),
2457 );
2458
2459 assert_migrate_with_migrations(
2460 &[MigrationType::Json(
2461 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2462 )],
2463 &r#"{
2464 "agent": {
2465 "play_sound_when_agent_done": "when_hidden"
2466 }
2467 }"#
2468 .unindent(),
2469 None,
2470 );
2471
2472 // Platform key: settings nested inside "macos" should be migrated
2473 assert_migrate_with_migrations(
2474 &[MigrationType::Json(
2475 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2476 )],
2477 &r#"
2478 {
2479 "macos": {
2480 "agent": {
2481 "play_sound_when_agent_done": true
2482 }
2483 }
2484 }
2485 "#
2486 .unindent(),
2487 Some(
2488 &r#"
2489 {
2490 "macos": {
2491 "agent": {
2492 "play_sound_when_agent_done": "always"
2493 }
2494 }
2495 }
2496 "#
2497 .unindent(),
2498 ),
2499 );
2500
2501 // Profile: settings nested inside profiles should be migrated
2502 assert_migrate_with_migrations(
2503 &[MigrationType::Json(
2504 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2505 )],
2506 &r#"
2507 {
2508 "profiles": {
2509 "work": {
2510 "agent": {
2511 "play_sound_when_agent_done": false
2512 }
2513 }
2514 }
2515 }
2516 "#
2517 .unindent(),
2518 Some(
2519 &r#"
2520 {
2521 "profiles": {
2522 "work": {
2523 "agent": {
2524 "play_sound_when_agent_done": "never"
2525 }
2526 }
2527 }
2528 }
2529 "#
2530 .unindent(),
2531 ),
2532 );
2533 }
2534
2535 #[test]
2536 fn test_remove_context_server_source() {
2537 assert_migrate_settings(
2538 &r#"
2539 {
2540 "context_servers": {
2541 "extension_server": {
2542 "source": "extension",
2543 "settings": {
2544 "foo": "bar"
2545 }
2546 },
2547 "custom_server": {
2548 "source": "custom",
2549 "command": "foo",
2550 "args": ["bar"],
2551 "env": {
2552 "FOO": "BAR"
2553 }
2554 },
2555 }
2556 }
2557 "#
2558 .unindent(),
2559 Some(
2560 &r#"
2561 {
2562 "context_servers": {
2563 "extension_server": {
2564 "settings": {
2565 "foo": "bar"
2566 }
2567 },
2568 "custom_server": {
2569 "command": "foo",
2570 "args": ["bar"],
2571 "env": {
2572 "FOO": "BAR"
2573 }
2574 },
2575 }
2576 }
2577 "#
2578 .unindent(),
2579 ),
2580 );
2581
2582 // Platform key: settings nested inside "linux" should be migrated
2583 assert_migrate_with_migrations(
2584 &[MigrationType::Json(
2585 migrations::m_2025_11_25::remove_context_server_source,
2586 )],
2587 &r#"
2588 {
2589 "linux": {
2590 "context_servers": {
2591 "my_server": {
2592 "source": "extension",
2593 "settings": {
2594 "key": "value"
2595 }
2596 }
2597 }
2598 }
2599 }
2600 "#
2601 .unindent(),
2602 Some(
2603 &r#"
2604 {
2605 "linux": {
2606 "context_servers": {
2607 "my_server": {
2608 "settings": {
2609 "key": "value"
2610 }
2611 }
2612 }
2613 }
2614 }
2615 "#
2616 .unindent(),
2617 ),
2618 );
2619
2620 // Profile: settings nested inside profiles should be migrated
2621 assert_migrate_with_migrations(
2622 &[MigrationType::Json(
2623 migrations::m_2025_11_25::remove_context_server_source,
2624 )],
2625 &r#"
2626 {
2627 "profiles": {
2628 "work": {
2629 "context_servers": {
2630 "my_server": {
2631 "source": "custom",
2632 "command": "foo",
2633 "args": ["bar"]
2634 }
2635 }
2636 }
2637 }
2638 }
2639 "#
2640 .unindent(),
2641 Some(
2642 &r#"
2643 {
2644 "profiles": {
2645 "work": {
2646 "context_servers": {
2647 "my_server": {
2648 "command": "foo",
2649 "args": ["bar"]
2650 }
2651 }
2652 }
2653 }
2654 }
2655 "#
2656 .unindent(),
2657 ),
2658 );
2659 }
2660
2661 #[test]
2662 fn test_project_panel_open_file_on_paste_migration() {
2663 assert_migrate_settings(
2664 &r#"
2665 {
2666 "project_panel": {
2667 "open_file_on_paste": true
2668 }
2669 }
2670 "#
2671 .unindent(),
2672 Some(
2673 &r#"
2674 {
2675 "project_panel": {
2676 "auto_open": { "on_paste": true }
2677 }
2678 }
2679 "#
2680 .unindent(),
2681 ),
2682 );
2683
2684 assert_migrate_settings(
2685 &r#"
2686 {
2687 "project_panel": {
2688 "open_file_on_paste": false
2689 }
2690 }
2691 "#
2692 .unindent(),
2693 Some(
2694 &r#"
2695 {
2696 "project_panel": {
2697 "auto_open": { "on_paste": false }
2698 }
2699 }
2700 "#
2701 .unindent(),
2702 ),
2703 );
2704 }
2705
2706 #[test]
2707 fn test_enable_preview_from_code_navigation_migration() {
2708 assert_migrate_settings(
2709 &r#"
2710 {
2711 "other_setting_1": 1,
2712 "preview_tabs": {
2713 "other_setting_2": 2,
2714 "enable_preview_from_code_navigation": false
2715 }
2716 }
2717 "#
2718 .unindent(),
2719 Some(
2720 &r#"
2721 {
2722 "other_setting_1": 1,
2723 "preview_tabs": {
2724 "other_setting_2": 2,
2725 "enable_keep_preview_on_code_navigation": false
2726 }
2727 }
2728 "#
2729 .unindent(),
2730 ),
2731 );
2732
2733 assert_migrate_settings(
2734 &r#"
2735 {
2736 "other_setting_1": 1,
2737 "preview_tabs": {
2738 "other_setting_2": 2,
2739 "enable_preview_from_code_navigation": true
2740 }
2741 }
2742 "#
2743 .unindent(),
2744 Some(
2745 &r#"
2746 {
2747 "other_setting_1": 1,
2748 "preview_tabs": {
2749 "other_setting_2": 2,
2750 "enable_keep_preview_on_code_navigation": true
2751 }
2752 }
2753 "#
2754 .unindent(),
2755 ),
2756 );
2757 }
2758
2759 #[test]
2760 fn test_make_auto_indent_an_enum() {
2761 // Empty settings should not change
2762 assert_migrate_with_migrations(
2763 &[MigrationType::Json(
2764 migrations::m_2025_01_27::make_auto_indent_an_enum,
2765 )],
2766 &r#"{ }"#.unindent(),
2767 None,
2768 );
2769
2770 // true should become "syntax_aware"
2771 assert_migrate_with_migrations(
2772 &[MigrationType::Json(
2773 migrations::m_2025_01_27::make_auto_indent_an_enum,
2774 )],
2775 &r#"{
2776 "auto_indent": true
2777 }"#
2778 .unindent(),
2779 Some(
2780 &r#"{
2781 "auto_indent": "syntax_aware"
2782 }"#
2783 .unindent(),
2784 ),
2785 );
2786
2787 // false should become "none"
2788 assert_migrate_with_migrations(
2789 &[MigrationType::Json(
2790 migrations::m_2025_01_27::make_auto_indent_an_enum,
2791 )],
2792 &r#"{
2793 "auto_indent": false
2794 }"#
2795 .unindent(),
2796 Some(
2797 &r#"{
2798 "auto_indent": "none"
2799 }"#
2800 .unindent(),
2801 ),
2802 );
2803
2804 // Already valid enum values should not change
2805 assert_migrate_with_migrations(
2806 &[MigrationType::Json(
2807 migrations::m_2025_01_27::make_auto_indent_an_enum,
2808 )],
2809 &r#"{
2810 "auto_indent": "preserve_indent"
2811 }"#
2812 .unindent(),
2813 None,
2814 );
2815
2816 // Should also work inside languages
2817 assert_migrate_with_migrations(
2818 &[MigrationType::Json(
2819 migrations::m_2025_01_27::make_auto_indent_an_enum,
2820 )],
2821 &r#"{
2822 "auto_indent": true,
2823 "languages": {
2824 "Python": {
2825 "auto_indent": false
2826 }
2827 }
2828 }"#
2829 .unindent(),
2830 Some(
2831 &r#"{
2832 "auto_indent": "syntax_aware",
2833 "languages": {
2834 "Python": {
2835 "auto_indent": "none"
2836 }
2837 }
2838 }"#
2839 .unindent(),
2840 ),
2841 );
2842 }
2843
2844 #[test]
2845 fn test_move_edit_prediction_provider_to_edit_predictions() {
2846 assert_migrate_with_migrations(
2847 &[MigrationType::Json(
2848 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2849 )],
2850 &r#"{ }"#.unindent(),
2851 None,
2852 );
2853
2854 assert_migrate_with_migrations(
2855 &[MigrationType::Json(
2856 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2857 )],
2858 &r#"
2859 {
2860 "features": {
2861 "edit_prediction_provider": "copilot"
2862 }
2863 }
2864 "#
2865 .unindent(),
2866 Some(
2867 &r#"
2868 {
2869 "edit_predictions": {
2870 "provider": "copilot"
2871 }
2872 }
2873 "#
2874 .unindent(),
2875 ),
2876 );
2877
2878 assert_migrate_with_migrations(
2879 &[MigrationType::Json(
2880 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2881 )],
2882 &r#"
2883 {
2884 "features": {
2885 "edit_prediction_provider": "zed"
2886 },
2887 "edit_predictions": {
2888 "mode": "eager"
2889 }
2890 }
2891 "#
2892 .unindent(),
2893 Some(
2894 &r#"
2895 {
2896 "edit_predictions": {
2897 "provider": "zed",
2898 "mode": "eager"
2899 }
2900 }
2901 "#
2902 .unindent(),
2903 ),
2904 );
2905
2906 assert_migrate_with_migrations(
2907 &[MigrationType::Json(
2908 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2909 )],
2910 &r#"
2911 {
2912 "features": {
2913 "edit_prediction_provider": "supermaven"
2914 },
2915 "edit_predictions": {
2916 "provider": "copilot"
2917 }
2918 }
2919 "#
2920 .unindent(),
2921 Some(
2922 &r#"
2923 {
2924 "edit_predictions": {
2925 "provider": "copilot"
2926 }
2927 }
2928 "#
2929 .unindent(),
2930 ),
2931 );
2932
2933 assert_migrate_with_migrations(
2934 &[MigrationType::Json(
2935 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2936 )],
2937 &r#"
2938 {
2939 "edit_predictions": {
2940 "provider": "zed"
2941 }
2942 }
2943 "#
2944 .unindent(),
2945 None,
2946 );
2947
2948 // Non-object edit_predictions (e.g. true) should gracefully skip
2949 // instead of bail!-ing and aborting the entire migration chain.
2950 assert_migrate_with_migrations(
2951 &[MigrationType::Json(
2952 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2953 )],
2954 &r#"
2955 {
2956 "features": {
2957 "edit_prediction_provider": "copilot"
2958 },
2959 "edit_predictions": true
2960 }
2961 "#
2962 .unindent(),
2963 Some(
2964 &r#"
2965 {
2966 "edit_predictions": true
2967 }
2968 "#
2969 .unindent(),
2970 ),
2971 );
2972
2973 // Platform key: settings nested inside "macos" should be migrated
2974 assert_migrate_with_migrations(
2975 &[MigrationType::Json(
2976 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2977 )],
2978 &r#"
2979 {
2980 "macos": {
2981 "features": {
2982 "edit_prediction_provider": "copilot"
2983 }
2984 }
2985 }
2986 "#
2987 .unindent(),
2988 Some(
2989 &r#"
2990 {
2991 "macos": {
2992 "edit_predictions": {
2993 "provider": "copilot"
2994 }
2995 }
2996 }
2997 "#
2998 .unindent(),
2999 ),
3000 );
3001
3002 // Profile: settings nested inside profiles should be migrated
3003 assert_migrate_with_migrations(
3004 &[MigrationType::Json(
3005 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
3006 )],
3007 &r#"
3008 {
3009 "profiles": {
3010 "work": {
3011 "features": {
3012 "edit_prediction_provider": "copilot"
3013 }
3014 }
3015 }
3016 }
3017 "#
3018 .unindent(),
3019 Some(
3020 &r#"
3021 {
3022 "profiles": {
3023 "work": {
3024 "edit_predictions": {
3025 "provider": "copilot"
3026 }
3027 }
3028 }
3029 }
3030 "#
3031 .unindent(),
3032 ),
3033 );
3034
3035 // Combined: root + platform + profile should all be migrated simultaneously
3036 assert_migrate_with_migrations(
3037 &[MigrationType::Json(
3038 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
3039 )],
3040 &r#"
3041 {
3042 "features": {
3043 "edit_prediction_provider": "copilot"
3044 },
3045 "macos": {
3046 "features": {
3047 "edit_prediction_provider": "zed"
3048 }
3049 },
3050 "profiles": {
3051 "work": {
3052 "features": {
3053 "edit_prediction_provider": "supermaven"
3054 }
3055 }
3056 }
3057 }
3058 "#
3059 .unindent(),
3060 Some(
3061 &r#"
3062 {
3063 "edit_predictions": {
3064 "provider": "copilot"
3065 },
3066 "macos": {
3067 "edit_predictions": {
3068 "provider": "zed"
3069 }
3070 },
3071 "profiles": {
3072 "work": {
3073 "edit_predictions": {
3074 "provider": "supermaven"
3075 }
3076 }
3077 }
3078 }
3079 "#
3080 .unindent(),
3081 ),
3082 );
3083 }
3084
3085 #[test]
3086 fn test_migrate_experimental_sweep_mercury() {
3087 assert_migrate_with_migrations(
3088 &[MigrationType::Json(
3089 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3090 )],
3091 &r#"{ }"#.unindent(),
3092 None,
3093 );
3094
3095 assert_migrate_with_migrations(
3096 &[MigrationType::Json(
3097 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3098 )],
3099 &r#"
3100 {
3101 "edit_predictions": {
3102 "provider": {
3103 "experimental": "sweep"
3104 }
3105 }
3106 }
3107 "#
3108 .unindent(),
3109 Some(
3110 &r#"
3111 {
3112 "edit_predictions": {
3113 "provider": "sweep"
3114 }
3115 }
3116 "#
3117 .unindent(),
3118 ),
3119 );
3120
3121 assert_migrate_with_migrations(
3122 &[MigrationType::Json(
3123 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3124 )],
3125 &r#"
3126 {
3127 "edit_predictions": {
3128 "provider": {
3129 "experimental": "mercury"
3130 }
3131 }
3132 }
3133 "#
3134 .unindent(),
3135 Some(
3136 &r#"
3137 {
3138 "edit_predictions": {
3139 "provider": "mercury"
3140 }
3141 }
3142 "#
3143 .unindent(),
3144 ),
3145 );
3146
3147 assert_migrate_with_migrations(
3148 &[MigrationType::Json(
3149 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3150 )],
3151 &r#"
3152 {
3153 "features": {
3154 "edit_prediction_provider": {
3155 "experimental": "sweep"
3156 }
3157 }
3158 }
3159 "#
3160 .unindent(),
3161 Some(
3162 &r#"
3163 {
3164 "features": {
3165 "edit_prediction_provider": "sweep"
3166 }
3167 }
3168 "#
3169 .unindent(),
3170 ),
3171 );
3172
3173 assert_migrate_with_migrations(
3174 &[MigrationType::Json(
3175 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3176 )],
3177 &r#"
3178 {
3179 "edit_predictions": {
3180 "provider": "zed"
3181 }
3182 }
3183 "#
3184 .unindent(),
3185 None,
3186 );
3187
3188 assert_migrate_with_migrations(
3189 &[MigrationType::Json(
3190 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3191 )],
3192 &r#"
3193 {
3194 "edit_predictions": {
3195 "provider": {
3196 "experimental": "zeta2"
3197 }
3198 }
3199 }
3200 "#
3201 .unindent(),
3202 None,
3203 );
3204
3205 // Platform key: settings nested inside "linux" should be migrated
3206 assert_migrate_with_migrations(
3207 &[MigrationType::Json(
3208 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3209 )],
3210 &r#"
3211 {
3212 "linux": {
3213 "edit_predictions": {
3214 "provider": {
3215 "experimental": "sweep"
3216 }
3217 }
3218 }
3219 }
3220 "#
3221 .unindent(),
3222 Some(
3223 &r#"
3224 {
3225 "linux": {
3226 "edit_predictions": {
3227 "provider": "sweep"
3228 }
3229 }
3230 }
3231 "#
3232 .unindent(),
3233 ),
3234 );
3235
3236 // Profile: settings nested inside profiles should be migrated
3237 assert_migrate_with_migrations(
3238 &[MigrationType::Json(
3239 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3240 )],
3241 &r#"
3242 {
3243 "profiles": {
3244 "dev": {
3245 "edit_predictions": {
3246 "provider": {
3247 "experimental": "mercury"
3248 }
3249 }
3250 }
3251 }
3252 }
3253 "#
3254 .unindent(),
3255 Some(
3256 &r#"
3257 {
3258 "profiles": {
3259 "dev": {
3260 "edit_predictions": {
3261 "provider": "mercury"
3262 }
3263 }
3264 }
3265 }
3266 "#
3267 .unindent(),
3268 ),
3269 );
3270
3271 // Combined: root + platform + profile should all be migrated simultaneously
3272 assert_migrate_with_migrations(
3273 &[MigrationType::Json(
3274 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3275 )],
3276 &r#"
3277 {
3278 "edit_predictions": {
3279 "provider": {
3280 "experimental": "sweep"
3281 }
3282 },
3283 "linux": {
3284 "edit_predictions": {
3285 "provider": {
3286 "experimental": "mercury"
3287 }
3288 }
3289 },
3290 "profiles": {
3291 "dev": {
3292 "edit_predictions": {
3293 "provider": {
3294 "experimental": "sweep"
3295 }
3296 }
3297 }
3298 }
3299 }
3300 "#
3301 .unindent(),
3302 Some(
3303 &r#"
3304 {
3305 "edit_predictions": {
3306 "provider": "sweep"
3307 },
3308 "linux": {
3309 "edit_predictions": {
3310 "provider": "mercury"
3311 }
3312 },
3313 "profiles": {
3314 "dev": {
3315 "edit_predictions": {
3316 "provider": "sweep"
3317 }
3318 }
3319 }
3320 }
3321 "#
3322 .unindent(),
3323 ),
3324 );
3325 }
3326
3327 #[test]
3328 fn test_migrate_always_allow_tool_actions_to_default() {
3329 // No agent settings - no change
3330 assert_migrate_with_migrations(
3331 &[MigrationType::Json(
3332 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3333 )],
3334 &r#"{ }"#.unindent(),
3335 None,
3336 );
3337
3338 // always_allow_tool_actions: true -> tool_permissions.default: "allow"
3339 assert_migrate_with_migrations(
3340 &[MigrationType::Json(
3341 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3342 )],
3343 &r#"
3344 {
3345 "agent": {
3346 "always_allow_tool_actions": true
3347 }
3348 }
3349 "#
3350 .unindent(),
3351 Some(
3352 &r#"
3353 {
3354 "agent": {
3355 "tool_permissions": {
3356 "default": "allow"
3357 }
3358 }
3359 }
3360 "#
3361 .unindent(),
3362 ),
3363 );
3364
3365 // always_allow_tool_actions: false -> just remove it
3366 assert_migrate_with_migrations(
3367 &[MigrationType::Json(
3368 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3369 )],
3370 &r#"
3371 {
3372 "agent": {
3373 "always_allow_tool_actions": false
3374 }
3375 }
3376 "#
3377 .unindent(),
3378 Some(
3379 // The blank line has spaces because the migration preserves the original indentation
3380 "{\n \"agent\": {\n \n }\n}\n",
3381 ),
3382 );
3383
3384 // Preserve existing tool_permissions.tools when migrating
3385 assert_migrate_with_migrations(
3386 &[MigrationType::Json(
3387 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3388 )],
3389 &r#"
3390 {
3391 "agent": {
3392 "always_allow_tool_actions": true,
3393 "tool_permissions": {
3394 "tools": {
3395 "terminal": {
3396 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3397 }
3398 }
3399 }
3400 }
3401 }
3402 "#
3403 .unindent(),
3404 Some(
3405 &r#"
3406 {
3407 "agent": {
3408 "tool_permissions": {
3409 "default": "allow",
3410 "tools": {
3411 "terminal": {
3412 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3413 }
3414 }
3415 }
3416 }
3417 }
3418 "#
3419 .unindent(),
3420 ),
3421 );
3422
3423 // Don't override existing default (and migrate default_mode to default)
3424 assert_migrate_with_migrations(
3425 &[MigrationType::Json(
3426 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3427 )],
3428 &r#"
3429 {
3430 "agent": {
3431 "always_allow_tool_actions": true,
3432 "tool_permissions": {
3433 "default_mode": "confirm"
3434 }
3435 }
3436 }
3437 "#
3438 .unindent(),
3439 Some(
3440 &r#"
3441 {
3442 "agent": {
3443 "tool_permissions": {
3444 "default": "confirm"
3445 }
3446 }
3447 }
3448 "#
3449 .unindent(),
3450 ),
3451 );
3452
3453 // Migrate existing default_mode to default (no always_allow_tool_actions)
3454 assert_migrate_with_migrations(
3455 &[MigrationType::Json(
3456 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3457 )],
3458 &r#"
3459 {
3460 "agent": {
3461 "tool_permissions": {
3462 "default_mode": "allow"
3463 }
3464 }
3465 }
3466 "#
3467 .unindent(),
3468 Some(
3469 &r#"
3470 {
3471 "agent": {
3472 "tool_permissions": {
3473 "default": "allow"
3474 }
3475 }
3476 }
3477 "#
3478 .unindent(),
3479 ),
3480 );
3481
3482 // No migration needed if already using new format with "default"
3483 assert_migrate_with_migrations(
3484 &[MigrationType::Json(
3485 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3486 )],
3487 &r#"
3488 {
3489 "agent": {
3490 "tool_permissions": {
3491 "default": "allow"
3492 }
3493 }
3494 }
3495 "#
3496 .unindent(),
3497 None,
3498 );
3499
3500 // Migrate default_mode to default in tool-specific rules
3501 assert_migrate_with_migrations(
3502 &[MigrationType::Json(
3503 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3504 )],
3505 &r#"
3506 {
3507 "agent": {
3508 "tool_permissions": {
3509 "default_mode": "confirm",
3510 "tools": {
3511 "terminal": {
3512 "default_mode": "allow"
3513 }
3514 }
3515 }
3516 }
3517 }
3518 "#
3519 .unindent(),
3520 Some(
3521 &r#"
3522 {
3523 "agent": {
3524 "tool_permissions": {
3525 "default": "confirm",
3526 "tools": {
3527 "terminal": {
3528 "default": "allow"
3529 }
3530 }
3531 }
3532 }
3533 }
3534 "#
3535 .unindent(),
3536 ),
3537 );
3538
3539 // When tool_permissions is null, replace it so always_allow is preserved
3540 assert_migrate_with_migrations(
3541 &[MigrationType::Json(
3542 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3543 )],
3544 &r#"
3545 {
3546 "agent": {
3547 "always_allow_tool_actions": true,
3548 "tool_permissions": null
3549 }
3550 }
3551 "#
3552 .unindent(),
3553 Some(
3554 &r#"
3555 {
3556 "agent": {
3557 "tool_permissions": {
3558 "default": "allow"
3559 }
3560 }
3561 }
3562 "#
3563 .unindent(),
3564 ),
3565 );
3566
3567 // Platform-specific agent migration
3568 assert_migrate_with_migrations(
3569 &[MigrationType::Json(
3570 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3571 )],
3572 &r#"
3573 {
3574 "linux": {
3575 "agent": {
3576 "always_allow_tool_actions": true
3577 }
3578 }
3579 }
3580 "#
3581 .unindent(),
3582 Some(
3583 &r#"
3584 {
3585 "linux": {
3586 "agent": {
3587 "tool_permissions": {
3588 "default": "allow"
3589 }
3590 }
3591 }
3592 }
3593 "#
3594 .unindent(),
3595 ),
3596 );
3597
3598 // Channel-specific agent migration
3599 assert_migrate_with_migrations(
3600 &[MigrationType::Json(
3601 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3602 )],
3603 &r#"
3604 {
3605 "agent": {
3606 "always_allow_tool_actions": true
3607 },
3608 "nightly": {
3609 "agent": {
3610 "tool_permissions": {
3611 "default_mode": "confirm"
3612 }
3613 }
3614 }
3615 }
3616 "#
3617 .unindent(),
3618 Some(
3619 &r#"
3620 {
3621 "agent": {
3622 "tool_permissions": {
3623 "default": "allow"
3624 }
3625 },
3626 "nightly": {
3627 "agent": {
3628 "tool_permissions": {
3629 "default": "confirm"
3630 }
3631 }
3632 }
3633 }
3634 "#
3635 .unindent(),
3636 ),
3637 );
3638
3639 // Profile-level migration
3640 assert_migrate_with_migrations(
3641 &[MigrationType::Json(
3642 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3643 )],
3644 &r#"
3645 {
3646 "agent": {
3647 "profiles": {
3648 "custom": {
3649 "always_allow_tool_actions": true,
3650 "tool_permissions": {
3651 "default_mode": "allow"
3652 }
3653 }
3654 }
3655 }
3656 }
3657 "#
3658 .unindent(),
3659 Some(
3660 &r#"
3661 {
3662 "agent": {
3663 "profiles": {
3664 "custom": {
3665 "tool_permissions": {
3666 "default": "allow"
3667 }
3668 }
3669 }
3670 }
3671 }
3672 "#
3673 .unindent(),
3674 ),
3675 );
3676
3677 // Platform-specific agent with profiles
3678 assert_migrate_with_migrations(
3679 &[MigrationType::Json(
3680 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3681 )],
3682 &r#"
3683 {
3684 "macos": {
3685 "agent": {
3686 "always_allow_tool_actions": true,
3687 "profiles": {
3688 "strict": {
3689 "tool_permissions": {
3690 "default_mode": "deny"
3691 }
3692 }
3693 }
3694 }
3695 }
3696 }
3697 "#
3698 .unindent(),
3699 Some(
3700 &r#"
3701 {
3702 "macos": {
3703 "agent": {
3704 "tool_permissions": {
3705 "default": "allow"
3706 },
3707 "profiles": {
3708 "strict": {
3709 "tool_permissions": {
3710 "default": "deny"
3711 }
3712 }
3713 }
3714 }
3715 }
3716 }
3717 "#
3718 .unindent(),
3719 ),
3720 );
3721
3722 // Root-level profile with always_allow_tool_actions
3723 assert_migrate_with_migrations(
3724 &[MigrationType::Json(
3725 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3726 )],
3727 &r#"
3728 {
3729 "profiles": {
3730 "work": {
3731 "agent": {
3732 "always_allow_tool_actions": true
3733 }
3734 }
3735 }
3736 }
3737 "#
3738 .unindent(),
3739 Some(
3740 &r#"
3741 {
3742 "profiles": {
3743 "work": {
3744 "agent": {
3745 "tool_permissions": {
3746 "default": "allow"
3747 }
3748 }
3749 }
3750 }
3751 }
3752 "#
3753 .unindent(),
3754 ),
3755 );
3756
3757 // Root-level profile with default_mode
3758 assert_migrate_with_migrations(
3759 &[MigrationType::Json(
3760 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3761 )],
3762 &r#"
3763 {
3764 "profiles": {
3765 "work": {
3766 "agent": {
3767 "tool_permissions": {
3768 "default_mode": "allow"
3769 }
3770 }
3771 }
3772 }
3773 }
3774 "#
3775 .unindent(),
3776 Some(
3777 &r#"
3778 {
3779 "profiles": {
3780 "work": {
3781 "agent": {
3782 "tool_permissions": {
3783 "default": "allow"
3784 }
3785 }
3786 }
3787 }
3788 }
3789 "#
3790 .unindent(),
3791 ),
3792 );
3793
3794 // Root-level profile + root-level agent both migrated
3795 assert_migrate_with_migrations(
3796 &[MigrationType::Json(
3797 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3798 )],
3799 &r#"
3800 {
3801 "agent": {
3802 "always_allow_tool_actions": true
3803 },
3804 "profiles": {
3805 "strict": {
3806 "agent": {
3807 "tool_permissions": {
3808 "default_mode": "deny"
3809 }
3810 }
3811 }
3812 }
3813 }
3814 "#
3815 .unindent(),
3816 Some(
3817 &r#"
3818 {
3819 "agent": {
3820 "tool_permissions": {
3821 "default": "allow"
3822 }
3823 },
3824 "profiles": {
3825 "strict": {
3826 "agent": {
3827 "tool_permissions": {
3828 "default": "deny"
3829 }
3830 }
3831 }
3832 }
3833 }
3834 "#
3835 .unindent(),
3836 ),
3837 );
3838
3839 // Non-boolean always_allow_tool_actions (string "true") is left in place
3840 // so the schema validator can report it, rather than silently dropping user data.
3841 assert_migrate_with_migrations(
3842 &[MigrationType::Json(
3843 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3844 )],
3845 &r#"
3846 {
3847 "agent": {
3848 "always_allow_tool_actions": "true"
3849 }
3850 }
3851 "#
3852 .unindent(),
3853 None,
3854 );
3855
3856 // null always_allow_tool_actions is removed (treated as false)
3857 assert_migrate_with_migrations(
3858 &[MigrationType::Json(
3859 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3860 )],
3861 &r#"
3862 {
3863 "agent": {
3864 "always_allow_tool_actions": null
3865 }
3866 }
3867 "#
3868 .unindent(),
3869 Some(&"{\n \"agent\": {\n \n }\n}\n"),
3870 );
3871
3872 // Project-local settings (.zed/settings.json) with always_allow_tool_actions
3873 // These files have no platform/channel overrides or root-level profiles.
3874 assert_migrate_with_migrations(
3875 &[MigrationType::Json(
3876 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3877 )],
3878 &r#"
3879 {
3880 "agent": {
3881 "always_allow_tool_actions": true,
3882 "tool_permissions": {
3883 "tools": {
3884 "terminal": {
3885 "default_mode": "confirm",
3886 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3887 }
3888 }
3889 }
3890 }
3891 }
3892 "#
3893 .unindent(),
3894 Some(
3895 &r#"
3896 {
3897 "agent": {
3898 "tool_permissions": {
3899 "default": "allow",
3900 "tools": {
3901 "terminal": {
3902 "default": "confirm",
3903 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3904 }
3905 }
3906 }
3907 }
3908 }
3909 "#
3910 .unindent(),
3911 ),
3912 );
3913
3914 // Project-local settings with only default_mode (no always_allow_tool_actions)
3915 assert_migrate_with_migrations(
3916 &[MigrationType::Json(
3917 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3918 )],
3919 &r#"
3920 {
3921 "agent": {
3922 "tool_permissions": {
3923 "default_mode": "deny"
3924 }
3925 }
3926 }
3927 "#
3928 .unindent(),
3929 Some(
3930 &r#"
3931 {
3932 "agent": {
3933 "tool_permissions": {
3934 "default": "deny"
3935 }
3936 }
3937 }
3938 "#
3939 .unindent(),
3940 ),
3941 );
3942
3943 // Project-local settings with no agent section at all - no change
3944 assert_migrate_with_migrations(
3945 &[MigrationType::Json(
3946 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3947 )],
3948 &r#"
3949 {
3950 "tab_size": 4,
3951 "format_on_save": "on"
3952 }
3953 "#
3954 .unindent(),
3955 None,
3956 );
3957
3958 // Existing agent_servers are left untouched
3959 assert_migrate_with_migrations(
3960 &[MigrationType::Json(
3961 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3962 )],
3963 &r#"
3964 {
3965 "agent": {
3966 "always_allow_tool_actions": true
3967 },
3968 "agent_servers": {
3969 "claude": {
3970 "default_mode": "plan"
3971 },
3972 "codex": {
3973 "default_mode": "read-only"
3974 }
3975 }
3976 }
3977 "#
3978 .unindent(),
3979 Some(
3980 &r#"
3981 {
3982 "agent": {
3983 "tool_permissions": {
3984 "default": "allow"
3985 }
3986 },
3987 "agent_servers": {
3988 "claude": {
3989 "default_mode": "plan"
3990 },
3991 "codex": {
3992 "default_mode": "read-only"
3993 }
3994 }
3995 }
3996 "#
3997 .unindent(),
3998 ),
3999 );
4000
4001 // Existing agent_servers are left untouched even with partial entries
4002 assert_migrate_with_migrations(
4003 &[MigrationType::Json(
4004 migrations::m_2026_02_04::migrate_tool_permission_defaults,
4005 )],
4006 &r#"
4007 {
4008 "agent": {
4009 "always_allow_tool_actions": true
4010 },
4011 "agent_servers": {
4012 "claude": {
4013 "default_mode": "plan"
4014 }
4015 }
4016 }
4017 "#
4018 .unindent(),
4019 Some(
4020 &r#"
4021 {
4022 "agent": {
4023 "tool_permissions": {
4024 "default": "allow"
4025 }
4026 },
4027 "agent_servers": {
4028 "claude": {
4029 "default_mode": "plan"
4030 }
4031 }
4032 }
4033 "#
4034 .unindent(),
4035 ),
4036 );
4037
4038 // always_allow_tool_actions: false leaves agent_servers untouched
4039 assert_migrate_with_migrations(
4040 &[MigrationType::Json(
4041 migrations::m_2026_02_04::migrate_tool_permission_defaults,
4042 )],
4043 &r#"
4044 {
4045 "agent": {
4046 "always_allow_tool_actions": false
4047 },
4048 "agent_servers": {
4049 "claude": {}
4050 }
4051 }
4052 "#
4053 .unindent(),
4054 Some(
4055 "{\n \"agent\": {\n \n },\n \"agent_servers\": {\n \"claude\": {}\n }\n}\n",
4056 ),
4057 );
4058 }
4059
4060 #[test]
4061 fn test_migrate_builtin_agent_servers_to_registry_simple() {
4062 assert_migrate_with_migrations(
4063 &[MigrationType::Json(
4064 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4065 )],
4066 r#"{
4067 "agent_servers": {
4068 "gemini": {
4069 "default_model": "gemini-2.0-flash"
4070 },
4071 "claude": {
4072 "default_mode": "plan"
4073 },
4074 "codex": {
4075 "default_model": "o4-mini"
4076 }
4077 }
4078}"#,
4079 Some(
4080 r#"{
4081 "agent_servers": {
4082 "codex-acp": {
4083 "type": "registry",
4084 "default_model": "o4-mini"
4085 },
4086 "claude-acp": {
4087 "type": "registry",
4088 "default_mode": "plan"
4089 },
4090 "gemini": {
4091 "type": "registry",
4092 "default_model": "gemini-2.0-flash"
4093 }
4094 }
4095}"#,
4096 ),
4097 );
4098 }
4099
4100 #[test]
4101 fn test_migrate_builtin_agent_servers_empty_entries() {
4102 assert_migrate_with_migrations(
4103 &[MigrationType::Json(
4104 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4105 )],
4106 r#"{
4107 "agent_servers": {
4108 "gemini": {},
4109 "claude": {},
4110 "codex": {}
4111 }
4112}"#,
4113 Some(
4114 r#"{
4115 "agent_servers": {
4116 "codex-acp": {
4117 "type": "registry"
4118 },
4119 "claude-acp": {
4120 "type": "registry"
4121 },
4122 "gemini": {
4123 "type": "registry"
4124 }
4125 }
4126}"#,
4127 ),
4128 );
4129 }
4130
4131 #[test]
4132 fn test_migrate_builtin_agent_servers_with_command() {
4133 assert_migrate_with_migrations(
4134 &[MigrationType::Json(
4135 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4136 )],
4137 r#"{
4138 "agent_servers": {
4139 "claude": {
4140 "command": "/usr/local/bin/claude",
4141 "args": ["--verbose"],
4142 "env": {"CLAUDE_KEY": "abc123"},
4143 "default_mode": "plan",
4144 "default_model": "claude-sonnet-4"
4145 }
4146 }
4147}"#,
4148 Some(
4149 r#"{
4150 "agent_servers": {
4151 "claude-acp-custom": {
4152 "type": "custom",
4153 "command": "/usr/local/bin/claude",
4154 "args": [
4155 "--verbose"
4156 ],
4157 "env": {
4158 "CLAUDE_KEY": "abc123"
4159 },
4160 "default_mode": "plan",
4161 "default_model": "claude-sonnet-4"
4162 }
4163 }
4164}"#,
4165 ),
4166 );
4167 }
4168
4169 #[test]
4170 fn test_migrate_builtin_agent_servers_gemini_with_command() {
4171 assert_migrate_with_migrations(
4172 &[MigrationType::Json(
4173 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4174 )],
4175 r#"{
4176 "agent_servers": {
4177 "gemini": {
4178 "command": "/opt/gemini/bin/gemini",
4179 "default_model": "gemini-2.0-flash"
4180 }
4181 }
4182}"#,
4183 Some(
4184 r#"{
4185 "agent_servers": {
4186 "gemini-custom": {
4187 "type": "custom",
4188 "command": "/opt/gemini/bin/gemini",
4189 "default_model": "gemini-2.0-flash"
4190 }
4191 }
4192}"#,
4193 ),
4194 );
4195 }
4196
4197 #[test]
4198 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_false() {
4199 assert_migrate_with_migrations(
4200 &[MigrationType::Json(
4201 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4202 )],
4203 r#"{
4204 "agent_servers": {
4205 "gemini": {
4206 "ignore_system_version": false,
4207 "default_model": "gemini-2.0-flash"
4208 }
4209 }
4210}"#,
4211 Some(
4212 r#"{
4213 "agent_servers": {
4214 "gemini-custom": {
4215 "type": "custom",
4216 "command": "gemini",
4217 "default_model": "gemini-2.0-flash"
4218 }
4219 }
4220}"#,
4221 ),
4222 );
4223 }
4224
4225 #[test]
4226 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_true() {
4227 assert_migrate_with_migrations(
4228 &[MigrationType::Json(
4229 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4230 )],
4231 r#"{
4232 "agent_servers": {
4233 "gemini": {
4234 "ignore_system_version": true,
4235 "default_model": "gemini-2.0-flash"
4236 }
4237 }
4238}"#,
4239 Some(
4240 r#"{
4241 "agent_servers": {
4242 "gemini": {
4243 "type": "registry",
4244 "default_model": "gemini-2.0-flash"
4245 }
4246 }
4247}"#,
4248 ),
4249 );
4250 }
4251
4252 #[test]
4253 fn test_migrate_builtin_agent_servers_already_typed_unchanged() {
4254 assert_migrate_with_migrations(
4255 &[MigrationType::Json(
4256 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4257 )],
4258 r#"{
4259 "agent_servers": {
4260 "gemini": {
4261 "type": "registry",
4262 "default_model": "gemini-2.0-flash"
4263 },
4264 "claude-acp": {
4265 "type": "registry",
4266 "default_mode": "plan"
4267 }
4268 }
4269}"#,
4270 None,
4271 );
4272 }
4273
4274 #[test]
4275 fn test_migrate_builtin_agent_servers_preserves_custom_entries() {
4276 assert_migrate_with_migrations(
4277 &[MigrationType::Json(
4278 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4279 )],
4280 r#"{
4281 "agent_servers": {
4282 "claude": {
4283 "default_mode": "plan"
4284 },
4285 "my-custom-agent": {
4286 "type": "custom",
4287 "command": "/path/to/agent"
4288 }
4289 }
4290}"#,
4291 Some(
4292 r#"{
4293 "agent_servers": {
4294 "claude-acp": {
4295 "type": "registry",
4296 "default_mode": "plan"
4297 },
4298 "my-custom-agent": {
4299 "type": "custom",
4300 "command": "/path/to/agent"
4301 }
4302 }
4303}"#,
4304 ),
4305 );
4306 }
4307
4308 #[test]
4309 fn test_migrate_builtin_agent_servers_target_already_exists() {
4310 assert_migrate_with_migrations(
4311 &[MigrationType::Json(
4312 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4313 )],
4314 r#"{
4315 "agent_servers": {
4316 "claude": {
4317 "default_mode": "plan"
4318 },
4319 "claude-acp": {
4320 "type": "registry",
4321 "default_model": "claude-sonnet-4"
4322 }
4323 }
4324}"#,
4325 Some(
4326 r#"{
4327 "agent_servers": {
4328 "claude-acp": {
4329 "type": "registry",
4330 "default_model": "claude-sonnet-4"
4331 }
4332 }
4333}"#,
4334 ),
4335 );
4336 }
4337
4338 #[test]
4339 fn test_migrate_builtin_agent_servers_no_agent_servers_key() {
4340 assert_migrate_with_migrations(
4341 &[MigrationType::Json(
4342 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4343 )],
4344 r#"{
4345 "agent": {
4346 "enabled": true
4347 }
4348}"#,
4349 None,
4350 );
4351 }
4352
4353 #[test]
4354 fn test_migrate_builtin_agent_servers_all_fields() {
4355 assert_migrate_with_migrations(
4356 &[MigrationType::Json(
4357 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4358 )],
4359 r#"{
4360 "agent_servers": {
4361 "codex": {
4362 "env": {"OPENAI_API_KEY": "sk-123"},
4363 "default_mode": "read-only",
4364 "default_model": "o4-mini",
4365 "favorite_models": ["o4-mini", "codex-mini-latest"],
4366 "default_config_options": {"approval_mode": "auto-edit"},
4367 "favorite_config_option_values": {"approval_mode": ["auto-edit", "suggest"]}
4368 }
4369 }
4370}"#,
4371 Some(
4372 r#"{
4373 "agent_servers": {
4374 "codex-acp": {
4375 "type": "registry",
4376 "env": {
4377 "OPENAI_API_KEY": "sk-123"
4378 },
4379 "default_mode": "read-only",
4380 "default_model": "o4-mini",
4381 "favorite_models": [
4382 "o4-mini",
4383 "codex-mini-latest"
4384 ],
4385 "default_config_options": {
4386 "approval_mode": "auto-edit"
4387 },
4388 "favorite_config_option_values": {
4389 "approval_mode": [
4390 "auto-edit",
4391 "suggest"
4392 ]
4393 }
4394 }
4395 }
4396}"#,
4397 ),
4398 );
4399 }
4400
4401 #[test]
4402 fn test_migrate_builtin_agent_servers_codex_with_command() {
4403 assert_migrate_with_migrations(
4404 &[MigrationType::Json(
4405 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4406 )],
4407 r#"{
4408 "agent_servers": {
4409 "codex": {
4410 "command": "/usr/local/bin/codex",
4411 "args": ["--full-auto"],
4412 "default_model": "o4-mini"
4413 }
4414 }
4415}"#,
4416 Some(
4417 r#"{
4418 "agent_servers": {
4419 "codex-acp-custom": {
4420 "type": "custom",
4421 "command": "/usr/local/bin/codex",
4422 "args": [
4423 "--full-auto"
4424 ],
4425 "default_model": "o4-mini"
4426 }
4427 }
4428}"#,
4429 ),
4430 );
4431 }
4432
4433 #[test]
4434 fn test_migrate_builtin_agent_servers_mixed_migrated_and_not() {
4435 assert_migrate_with_migrations(
4436 &[MigrationType::Json(
4437 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4438 )],
4439 r#"{
4440 "agent_servers": {
4441 "gemini": {
4442 "type": "registry",
4443 "default_model": "gemini-2.0-flash"
4444 },
4445 "claude": {
4446 "default_mode": "plan"
4447 },
4448 "codex": {}
4449 }
4450}"#,
4451 Some(
4452 r#"{
4453 "agent_servers": {
4454 "codex-acp": {
4455 "type": "registry"
4456 },
4457 "claude-acp": {
4458 "type": "registry",
4459 "default_mode": "plan"
4460 },
4461 "gemini": {
4462 "type": "registry",
4463 "default_model": "gemini-2.0-flash"
4464 }
4465 }
4466}"#,
4467 ),
4468 );
4469 }
4470
4471 #[test]
4472 fn test_migrate_edit_prediction_conflict_context() {
4473 assert_migrate_with_migrations(
4474 &[MigrationType::TreeSitter(
4475 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4476 &KEYMAP_QUERY_2026_03_23,
4477 )],
4478 &r#"
4479 [
4480 {
4481 "context": "Editor && edit_prediction_conflict",
4482 "bindings": {
4483 "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding
4484 }
4485 }
4486 ]
4487 "#.unindent(),
4488 Some(
4489 &r#"
4490 [
4491 {
4492 "context": "Editor && (edit_prediction && (showing_completions || in_leading_whitespace))",
4493 "bindings": {
4494 "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding
4495 }
4496 }
4497 ]
4498 "#.unindent(),
4499 ),
4500 );
4501
4502 assert_migrate_with_migrations(
4503 &[MigrationType::TreeSitter(
4504 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4505 &KEYMAP_QUERY_2026_03_23,
4506 )],
4507 &r#"
4508 [
4509 {
4510 "context": "Editor && edit_prediction_conflict && !showing_completions",
4511 "bindings": {
4512 // Here we don't require a modifier unless there's a language server completion
4513 "tab": "editor::AcceptEditPrediction"
4514 }
4515 }
4516 ]
4517 "#.unindent(),
4518 Some(
4519 &r#"
4520 [
4521 {
4522 "context": "Editor && (edit_prediction && in_leading_whitespace)",
4523 "bindings": {
4524 // Here we don't require a modifier unless there's a language server completion
4525 "tab": "editor::AcceptEditPrediction"
4526 }
4527 }
4528 ]
4529 "#.unindent(),
4530 ),
4531 );
4532
4533 assert_migrate_with_migrations(
4534 &[MigrationType::TreeSitter(
4535 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4536 &KEYMAP_QUERY_2026_03_23,
4537 )],
4538 &r#"
4539 [
4540 {
4541 "context": "Editor && edit_prediction_conflict && showing_completions",
4542 "bindings": {
4543 "tab": "editor::AcceptEditPrediction"
4544 }
4545 }
4546 ]
4547 "#
4548 .unindent(),
4549 Some(
4550 &r#"
4551 [
4552 {
4553 "context": "Editor && (edit_prediction && showing_completions)",
4554 "bindings": {
4555 "tab": "editor::AcceptEditPrediction"
4556 }
4557 }
4558 ]
4559 "#
4560 .unindent(),
4561 ),
4562 );
4563
4564 assert_migrate_with_migrations(
4565 &[MigrationType::TreeSitter(
4566 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4567 &KEYMAP_QUERY_2026_03_23,
4568 )],
4569 &r#"
4570 [
4571 {
4572 "context": "Editor && edit_prediction",
4573 "bindings": {
4574 "tab": "editor::AcceptEditPrediction",
4575 // Optional: This makes the default `alt-l` binding do nothing.
4576 "alt-l": null
4577 }
4578 },
4579 {
4580 "context": "Editor && edit_prediction_conflict",
4581 "bindings": {
4582 "alt-tab": "editor::AcceptEditPrediction",
4583 // Optional: This makes the default `alt-l` binding do nothing.
4584 "alt-l": null
4585 }
4586 },
4587 ]
4588 "#
4589 .unindent(),
4590 Some(
4591 &r#"
4592 [
4593 {
4594 "context": "Editor && edit_prediction",
4595 "bindings": {
4596 "tab": "editor::AcceptEditPrediction",
4597 // Optional: This makes the default `alt-l` binding do nothing.
4598 "alt-l": null
4599 }
4600 },
4601 {
4602 "context": "Editor && (edit_prediction && (showing_completions || in_leading_whitespace))",
4603 "bindings": {
4604 "alt-tab": "editor::AcceptEditPrediction",
4605 // Optional: This makes the default `alt-l` binding do nothing.
4606 "alt-l": null
4607 }
4608 },
4609 ]
4610 "#
4611 .unindent(),
4612 ),
4613 );
4614 }
4615
4616 #[test]
4617 fn test_restructure_profiles_with_settings_key() {
4618 assert_migrate_settings(
4619 &r#"
4620 {
4621 "buffer_font_size": 14,
4622 "profiles": {
4623 "Presenting": {
4624 "buffer_font_size": 20,
4625 "theme": "One Light"
4626 },
4627 "Minimal": {
4628 "vim_mode": true
4629 }
4630 }
4631 }
4632 "#
4633 .unindent(),
4634 Some(
4635 &r#"
4636 {
4637 "buffer_font_size": 14,
4638 "profiles": {
4639 "Presenting": {
4640 "settings": {
4641 "buffer_font_size": 20,
4642 "theme": "One Light"
4643 }
4644 },
4645 "Minimal": {
4646 "settings": {
4647 "vim_mode": true
4648 }
4649 }
4650 }
4651 }
4652 "#
4653 .unindent(),
4654 ),
4655 );
4656 }
4657
4658 #[test]
4659 fn test_restructure_profiles_with_settings_key_already_migrated() {
4660 assert_migrate_settings(
4661 &r#"
4662 {
4663 "profiles": {
4664 "Presenting": {
4665 "settings": {
4666 "buffer_font_size": 20
4667 }
4668 }
4669 }
4670 }
4671 "#
4672 .unindent(),
4673 None,
4674 );
4675 }
4676
4677 #[test]
4678 fn test_restructure_profiles_with_settings_key_no_profiles() {
4679 assert_migrate_settings(
4680 &r#"
4681 {
4682 "buffer_font_size": 14
4683 }
4684 "#
4685 .unindent(),
4686 None,
4687 );
4688 }
4689
4690 #[test]
4691 fn test_rename_web_search_to_search_web_in_tool_permissions() {
4692 assert_migrate_with_migrations(
4693 &[MigrationType::Json(
4694 migrations::m_2026_04_10::rename_web_search_to_search_web,
4695 )],
4696 &r#"
4697 {
4698 "agent": {
4699 "tool_permissions": {
4700 "tools": {
4701 "web_search": {
4702 "allow": true
4703 }
4704 }
4705 }
4706 }
4707 }
4708 "#
4709 .unindent(),
4710 Some(
4711 &r#"
4712 {
4713 "agent": {
4714 "tool_permissions": {
4715 "tools": {
4716 "search_web": {
4717 "allow": true
4718 }
4719 }
4720 }
4721 }
4722 }
4723 "#
4724 .unindent(),
4725 ),
4726 );
4727 }
4728
4729 #[test]
4730 fn test_rename_web_search_to_search_web_in_profiles() {
4731 assert_migrate_with_migrations(
4732 &[MigrationType::Json(
4733 migrations::m_2026_04_10::rename_web_search_to_search_web,
4734 )],
4735 &r#"
4736 {
4737 "agent": {
4738 "profiles": {
4739 "write": {
4740 "tools": {
4741 "web_search": false
4742 }
4743 }
4744 }
4745 }
4746 }
4747 "#
4748 .unindent(),
4749 Some(
4750 &r#"
4751 {
4752 "agent": {
4753 "profiles": {
4754 "write": {
4755 "tools": {
4756 "search_web": false
4757 }
4758 }
4759 }
4760 }
4761 }
4762 "#
4763 .unindent(),
4764 ),
4765 );
4766 }
4767
4768 #[test]
4769 fn test_rename_web_search_to_search_web_no_change_when_already_migrated() {
4770 assert_migrate_with_migrations(
4771 &[MigrationType::Json(
4772 migrations::m_2026_04_10::rename_web_search_to_search_web,
4773 )],
4774 &r#"
4775 {
4776 "agent": {
4777 "tool_permissions": {
4778 "tools": {
4779 "search_web": {
4780 "allow": true
4781 }
4782 }
4783 }
4784 }
4785 }
4786 "#
4787 .unindent(),
4788 None,
4789 );
4790 }
4791
4792 #[test]
4793 fn test_rename_web_search_to_search_web_no_clobber() {
4794 assert_migrate_with_migrations(
4795 &[MigrationType::Json(
4796 migrations::m_2026_04_10::rename_web_search_to_search_web,
4797 )],
4798 &r#"
4799 {
4800 "agent": {
4801 "tool_permissions": {
4802 "tools": {
4803 "web_search": {
4804 "allow": false
4805 },
4806 "search_web": {
4807 "allow": true
4808 }
4809 }
4810 }
4811 }
4812 }
4813 "#
4814 .unindent(),
4815 Some(
4816 &r#"
4817 {
4818 "agent": {
4819 "tool_permissions": {
4820 "tools": {
4821 "search_web": {
4822 "allow": false
4823 }
4824 }
4825 }
4826 }
4827 }
4828 "#
4829 .unindent(),
4830 ),
4831 );
4832 }
4833
4834 #[test]
4835 fn test_rename_web_search_to_search_web_platform_override() {
4836 assert_migrate_with_migrations(
4837 &[MigrationType::Json(
4838 migrations::m_2026_04_10::rename_web_search_to_search_web,
4839 )],
4840 &r#"
4841 {
4842 "linux": {
4843 "agent": {
4844 "tool_permissions": {
4845 "tools": {
4846 "web_search": {
4847 "allow": true
4848 }
4849 }
4850 }
4851 }
4852 }
4853 }
4854 "#
4855 .unindent(),
4856 Some(
4857 &r#"
4858 {
4859 "linux": {
4860 "agent": {
4861 "tool_permissions": {
4862 "tools": {
4863 "search_web": {
4864 "allow": true
4865 }
4866 }
4867 }
4868 }
4869 }
4870 }
4871 "#
4872 .unindent(),
4873 ),
4874 );
4875 }
4876
4877 #[test]
4878 fn test_rename_web_search_to_search_web_release_channel_override() {
4879 assert_migrate_with_migrations(
4880 &[MigrationType::Json(
4881 migrations::m_2026_04_10::rename_web_search_to_search_web,
4882 )],
4883 &r#"
4884 {
4885 "nightly": {
4886 "agent": {
4887 "tool_permissions": {
4888 "tools": {
4889 "web_search": {
4890 "default": "allow"
4891 }
4892 }
4893 }
4894 }
4895 }
4896 }
4897 "#
4898 .unindent(),
4899 Some(
4900 &r#"
4901 {
4902 "nightly": {
4903 "agent": {
4904 "tool_permissions": {
4905 "tools": {
4906 "search_web": {
4907 "default": "allow"
4908 }
4909 }
4910 }
4911 }
4912 }
4913 }
4914 "#
4915 .unindent(),
4916 ),
4917 );
4918 }
4919
4920 #[test]
4921 fn test_rename_web_search_to_search_web_no_agent() {
4922 assert_migrate_with_migrations(
4923 &[MigrationType::Json(
4924 migrations::m_2026_04_10::rename_web_search_to_search_web,
4925 )],
4926 &r#"
4927 {
4928 "buffer_font_size": 14
4929 }
4930 "#
4931 .unindent(),
4932 None,
4933 );
4934 }
4935
4936 #[test]
4937 fn test_migration_helpers_handle_various_profile_forms() {
4938 let setting = "a_setting";
4939 let old_value = "old_value";
4940 let new_value = "new_value";
4941
4942 fn language_setting_fn(value: &mut serde_json::Value, _: &[&str]) -> anyhow::Result<()> {
4943 if let Some(obj) = value.as_object_mut() {
4944 if let Some(v) = obj.get_mut("a_setting") {
4945 *v = serde_json::json!("new_value");
4946 }
4947 }
4948 Ok(())
4949 }
4950
4951 let mut settings_fn = |map: &mut serde_json::Map<String, serde_json::Value>| {
4952 if let Some(v) = map.get_mut(setting) {
4953 *v = serde_json::json!(new_value);
4954 }
4955 Ok(())
4956 };
4957
4958 // Legacy form
4959 let input = serde_json::json!({
4960 "profiles": {
4961 "work": {
4962 setting: old_value
4963 }
4964 }
4965 });
4966 let expected = serde_json::json!({
4967 "profiles": {
4968 "work": {
4969 setting: new_value
4970 }
4971 }
4972 });
4973
4974 let mut value = input.clone();
4975 migrations::migrate_settings(&mut value, &mut settings_fn).unwrap();
4976 assert_eq!(value, expected);
4977
4978 let mut value = input;
4979 migrations::migrate_language_setting(&mut value, language_setting_fn).unwrap();
4980 assert_eq!(value, expected);
4981
4982 // Form after migration: `m_2026_04_01`
4983 let input = serde_json::json!({
4984 "profiles": {
4985 "work": {
4986 "settings": {
4987 setting: old_value
4988 }
4989 }
4990 }
4991 });
4992 let expected = serde_json::json!({
4993 "profiles": {
4994 "work": {
4995 "settings": {
4996 setting: new_value
4997 }
4998 }
4999 }
5000 });
5001
5002 let mut value = input.clone();
5003 migrations::migrate_settings(&mut value, &mut settings_fn).unwrap();
5004 assert_eq!(value, expected);
5005
5006 let mut value = input;
5007 migrations::migrate_language_setting(&mut value, language_setting_fn).unwrap();
5008 assert_eq!(value, expected);
5009
5010 // Base-only form after migration: `m_2026_04_01` (no settings to migrate)
5011 let input = serde_json::json!({
5012 "profiles": {
5013 "work": {
5014 "base": "default"
5015 }
5016 }
5017 });
5018
5019 let mut value = input.clone();
5020 migrations::migrate_settings(&mut value, &mut settings_fn).unwrap();
5021 assert_eq!(value, input);
5022
5023 let mut value = input.clone();
5024 migrations::migrate_language_setting(&mut value, language_setting_fn).unwrap();
5025 assert_eq!(value, input);
5026 }
5027
5028 #[test]
5029 fn test_rename_web_search_to_search_web_root_level_profile() {
5030 assert_migrate_with_migrations(
5031 &[MigrationType::Json(
5032 migrations::m_2026_04_10::rename_web_search_to_search_web,
5033 )],
5034 &r#"
5035 {
5036 "profiles": {
5037 "Work": {
5038 "settings": {
5039 "agent": {
5040 "tool_permissions": {
5041 "tools": {
5042 "web_search": {
5043 "default": "allow"
5044 }
5045 }
5046 }
5047 }
5048 }
5049 }
5050 }
5051 }
5052 "#
5053 .unindent(),
5054 Some(
5055 &r#"
5056 {
5057 "profiles": {
5058 "Work": {
5059 "settings": {
5060 "agent": {
5061 "tool_permissions": {
5062 "tools": {
5063 "search_web": {
5064 "default": "allow"
5065 }
5066 }
5067 }
5068 }
5069 }
5070 }
5071 }
5072 }
5073 "#
5074 .unindent(),
5075 ),
5076 );
5077 }
5078
5079 #[test]
5080 fn test_mcp_settings_migration_adds_settings_to_extension_servers() {
5081 assert_migrate_settings(
5082 r#"{
5083 "context_servers": {
5084 "extension_server": {},
5085 "stdio_server": {
5086 "command": "npx",
5087 "args": ["-y", "some-server"]
5088 },
5089 "http_server": {
5090 "url": "https://example.com/mcp"
5091 },
5092 "http_server_with_headers": {
5093 "url": "https://example.com/mcp",
5094 "headers": {
5095 "Authorization": "Bearer token"
5096 }
5097 }
5098 }
5099}"#,
5100 Some(
5101 r#"{
5102 "context_servers": {
5103 "extension_server": {
5104 "settings": {}
5105 },
5106 "stdio_server": {
5107 "command": "npx",
5108 "args": ["-y", "some-server"]
5109 },
5110 "http_server": {
5111 "url": "https://example.com/mcp"
5112 },
5113 "http_server_with_headers": {
5114 "url": "https://example.com/mcp",
5115 "headers": {
5116 "Authorization": "Bearer token"
5117 }
5118 }
5119 }
5120}"#,
5121 ),
5122 );
5123 }
5124
5125 #[test]
5126 fn test_promote_show_branch_icon_true_to_show_branch_status_icon_at_root() {
5127 assert_migrate_settings(
5128 &r#"
5129 {
5130 "title_bar": {
5131 "show_branch_icon": true,
5132 "show_branch_name": true
5133 }
5134 }
5135 "#
5136 .unindent(),
5137 Some(
5138 &r#"
5139 {
5140 "title_bar": {
5141 "show_branch_status_icon": true,
5142 "show_branch_name": true
5143 }
5144 }
5145 "#
5146 .unindent(),
5147 ),
5148 );
5149 }
5150
5151 #[test]
5152 fn test_drop_show_branch_icon_false_without_setting_status_icon() {
5153 assert_migrate_settings(
5154 &r#"
5155 {
5156 "title_bar": {
5157 "show_branch_icon": false,
5158 "show_branch_name": true
5159 }
5160 }
5161 "#
5162 .unindent(),
5163 Some(
5164 &r#"
5165 {
5166 "title_bar": {
5167 "show_branch_name": true
5168 }
5169 }
5170 "#
5171 .unindent(),
5172 ),
5173 );
5174 }
5175
5176 #[test]
5177 fn test_promote_show_branch_icon_true_to_show_branch_status_icon_in_platform_override() {
5178 assert_migrate_settings(
5179 &r#"
5180 {
5181 "macos": {
5182 "title_bar": {
5183 "show_branch_icon": true,
5184 "show_branch_name": true
5185 }
5186 }
5187 }
5188 "#
5189 .unindent(),
5190 Some(
5191 &r#"
5192 {
5193 "macos": {
5194 "title_bar": {
5195 "show_branch_status_icon": true,
5196 "show_branch_name": true
5197 }
5198 }
5199 }
5200 "#
5201 .unindent(),
5202 ),
5203 );
5204 }
5205
5206 #[test]
5207 fn test_promote_show_branch_icon_true_to_show_branch_status_icon_in_release_override() {
5208 assert_migrate_settings(
5209 &r#"
5210 {
5211 "preview": {
5212 "title_bar": {
5213 "show_branch_icon": true,
5214 "show_branch_name": true
5215 }
5216 }
5217 }
5218 "#
5219 .unindent(),
5220 Some(
5221 &r#"
5222 {
5223 "preview": {
5224 "title_bar": {
5225 "show_branch_status_icon": true,
5226 "show_branch_name": true
5227 }
5228 }
5229 }
5230 "#
5231 .unindent(),
5232 ),
5233 );
5234 }
5235
5236 #[test]
5237 fn test_promote_show_branch_icon_true_to_show_branch_status_icon_in_profiles() {
5238 assert_migrate_settings(
5239 &r#"
5240 {
5241 "profiles": {
5242 "work": {
5243 "title_bar": {
5244 "show_branch_icon": true,
5245 "show_branch_name": true
5246 }
5247 }
5248 }
5249 }
5250 "#
5251 .unindent(),
5252 Some(
5253 &r#"
5254 {
5255 "profiles": {
5256 "work": {
5257 "settings": {
5258 "title_bar": {
5259 "show_branch_status_icon": true,
5260 "show_branch_name": true
5261 }
5262 }
5263 }
5264 }
5265 }
5266 "#
5267 .unindent(),
5268 ),
5269 );
5270 }
5271
5272 #[test]
5273 fn test_promote_show_branch_icon_true_to_show_branch_status_icon_across_all_scopes() {
5274 assert_migrate_settings(
5275 &r#"
5276 {
5277 "title_bar": {
5278 "show_branch_icon": true,
5279 "show_branch_name": true
5280 },
5281 "macos": {
5282 "title_bar": {
5283 "show_branch_icon": true,
5284 "show_branch_name": true
5285 }
5286 },
5287 "preview": {
5288 "title_bar": {
5289 "show_branch_icon": true,
5290 "show_branch_name": true
5291 }
5292 },
5293 "profiles": {
5294 "work": {
5295 "title_bar": {
5296 "show_branch_icon": true,
5297 "show_branch_name": true
5298 }
5299 }
5300 }
5301 }
5302 "#
5303 .unindent(),
5304 Some(
5305 &r#"
5306 {
5307 "title_bar": {
5308 "show_branch_status_icon": true,
5309 "show_branch_name": true
5310 },
5311 "macos": {
5312 "title_bar": {
5313 "show_branch_status_icon": true,
5314 "show_branch_name": true
5315 }
5316 },
5317 "preview": {
5318 "title_bar": {
5319 "show_branch_status_icon": true,
5320 "show_branch_name": true
5321 }
5322 },
5323 "profiles": {
5324 "work": {
5325 "settings": {
5326 "title_bar": {
5327 "show_branch_status_icon": true,
5328 "show_branch_name": true
5329 }
5330 }
5331 }
5332 }
5333 }
5334 "#
5335 .unindent(),
5336 ),
5337 );
5338 }
5339
5340 #[test]
5341 fn test_promote_show_branch_icon_true_to_show_branch_status_icon_no_change_when_already_migrated()
5342 {
5343 assert_migrate_settings(
5344 &r#"
5345 {
5346 "title_bar": {
5347 "show_branch_status_icon": true,
5348 "show_branch_name": true
5349 }
5350 }
5351 "#
5352 .unindent(),
5353 None,
5354 );
5355
5356 // No title_bar key — should be unchanged
5357 assert_migrate_settings(&r#"{ "theme": "One Dark" }"#.unindent(), None);
5358
5359 // title_bar without show_branch_icon — should be unchanged
5360 assert_migrate_settings(
5361 &r#"
5362 {
5363 "title_bar": {
5364 "show_branch_name": true
5365 }
5366 }
5367 "#
5368 .unindent(),
5369 None,
5370 );
5371 }
5372}