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 std::{cmp::Reverse, ops::Range, sync::LazyLock};
19use streaming_iterator::StreamingIterator;
20use tree_sitter::{Query, QueryMatch};
21
22use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
23
24mod migrations;
25mod patterns;
26
27fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
28 let mut parser = tree_sitter::Parser::new();
29 parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
30 let syntax_tree = parser
31 .parse(&text, None)
32 .context("failed to parse settings")?;
33
34 let mut cursor = tree_sitter::QueryCursor::new();
35 let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
36
37 let mut edits = vec![];
38 while let Some(mat) = matches.next() {
39 if let Some((_, callback)) = patterns.get(mat.pattern_index) {
40 edits.extend(callback(&text, &mat, query));
41 }
42 }
43
44 edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
45 edits.dedup_by(|(range_b, _), (range_a, _)| {
46 range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
47 });
48
49 if edits.is_empty() {
50 Ok(None)
51 } else {
52 let mut new_text = text.to_string();
53 for (range, replacement) in edits.iter().rev() {
54 new_text.replace_range(range.clone(), replacement);
55 }
56 if new_text == text {
57 log::error!(
58 "Edits computed for configuration migration do not cause a change: {:?}",
59 edits
60 );
61 Ok(None)
62 } else {
63 Ok(Some(new_text))
64 }
65 }
66}
67
68fn run_migrations(
69 text: &str,
70 migrations: &[(MigrationPatterns, &Query)],
71) -> Result<Option<String>> {
72 let mut current_text = text.to_string();
73 let mut result: Option<String> = None;
74 for (patterns, query) in migrations.iter() {
75 if let Some(migrated_text) = migrate(¤t_text, patterns, query)? {
76 current_text = migrated_text.clone();
77 result = Some(migrated_text);
78 }
79 }
80 Ok(result.filter(|new_text| text != new_text))
81}
82
83pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
84 let migrations: &[(MigrationPatterns, &Query)] = &[
85 (
86 migrations::m_2025_01_29::KEYMAP_PATTERNS,
87 &KEYMAP_QUERY_2025_01_29,
88 ),
89 (
90 migrations::m_2025_01_30::KEYMAP_PATTERNS,
91 &KEYMAP_QUERY_2025_01_30,
92 ),
93 (
94 migrations::m_2025_03_03::KEYMAP_PATTERNS,
95 &KEYMAP_QUERY_2025_03_03,
96 ),
97 (
98 migrations::m_2025_03_06::KEYMAP_PATTERNS,
99 &KEYMAP_QUERY_2025_03_06,
100 ),
101 (
102 migrations::m_2025_04_15::KEYMAP_PATTERNS,
103 &KEYMAP_QUERY_2025_04_15,
104 ),
105 ];
106 run_migrations(text, migrations)
107}
108
109pub fn migrate_settings(text: &str) -> Result<Option<String>> {
110 let migrations: &[(MigrationPatterns, &Query)] = &[
111 (
112 migrations::m_2025_01_02::SETTINGS_PATTERNS,
113 &SETTINGS_QUERY_2025_01_02,
114 ),
115 (
116 migrations::m_2025_01_29::SETTINGS_PATTERNS,
117 &SETTINGS_QUERY_2025_01_29,
118 ),
119 (
120 migrations::m_2025_01_30::SETTINGS_PATTERNS,
121 &SETTINGS_QUERY_2025_01_30,
122 ),
123 (
124 migrations::m_2025_03_29::SETTINGS_PATTERNS,
125 &SETTINGS_QUERY_2025_03_29,
126 ),
127 (
128 migrations::m_2025_04_15::SETTINGS_PATTERNS,
129 &SETTINGS_QUERY_2025_04_15,
130 ),
131 (
132 migrations::m_2025_04_21::SETTINGS_PATTERNS,
133 &SETTINGS_QUERY_2025_04_21,
134 ),
135 (
136 migrations::m_2025_04_23::SETTINGS_PATTERNS,
137 &SETTINGS_QUERY_2025_04_23,
138 ),
139 (
140 migrations::m_2025_05_05::SETTINGS_PATTERNS,
141 &SETTINGS_QUERY_2025_05_05,
142 ),
143 (
144 migrations::m_2025_05_08::SETTINGS_PATTERNS,
145 &SETTINGS_QUERY_2025_05_08,
146 ),
147 (
148 migrations::m_2025_05_29::SETTINGS_PATTERNS,
149 &SETTINGS_QUERY_2025_05_29,
150 ),
151 (
152 migrations::m_2025_06_16::SETTINGS_PATTERNS,
153 &SETTINGS_QUERY_2025_06_16,
154 ),
155 ];
156 run_migrations(text, migrations)
157}
158
159pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
160 migrate(
161 &text,
162 &[(
163 SETTINGS_NESTED_KEY_VALUE_PATTERN,
164 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
165 )],
166 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
167 )
168}
169
170pub type MigrationPatterns = &'static [(
171 &'static str,
172 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
173)];
174
175macro_rules! define_query {
176 ($var_name:ident, $patterns_path:path) => {
177 static $var_name: LazyLock<Query> = LazyLock::new(|| {
178 Query::new(
179 &tree_sitter_json::LANGUAGE.into(),
180 &$patterns_path
181 .iter()
182 .map(|pattern| pattern.0)
183 .collect::<String>(),
184 )
185 .unwrap()
186 });
187 };
188}
189
190// keymap
191define_query!(
192 KEYMAP_QUERY_2025_01_29,
193 migrations::m_2025_01_29::KEYMAP_PATTERNS
194);
195define_query!(
196 KEYMAP_QUERY_2025_01_30,
197 migrations::m_2025_01_30::KEYMAP_PATTERNS
198);
199define_query!(
200 KEYMAP_QUERY_2025_03_03,
201 migrations::m_2025_03_03::KEYMAP_PATTERNS
202);
203define_query!(
204 KEYMAP_QUERY_2025_03_06,
205 migrations::m_2025_03_06::KEYMAP_PATTERNS
206);
207define_query!(
208 KEYMAP_QUERY_2025_04_15,
209 migrations::m_2025_04_15::KEYMAP_PATTERNS
210);
211
212// settings
213define_query!(
214 SETTINGS_QUERY_2025_01_02,
215 migrations::m_2025_01_02::SETTINGS_PATTERNS
216);
217define_query!(
218 SETTINGS_QUERY_2025_01_29,
219 migrations::m_2025_01_29::SETTINGS_PATTERNS
220);
221define_query!(
222 SETTINGS_QUERY_2025_01_30,
223 migrations::m_2025_01_30::SETTINGS_PATTERNS
224);
225define_query!(
226 SETTINGS_QUERY_2025_03_29,
227 migrations::m_2025_03_29::SETTINGS_PATTERNS
228);
229define_query!(
230 SETTINGS_QUERY_2025_04_15,
231 migrations::m_2025_04_15::SETTINGS_PATTERNS
232);
233define_query!(
234 SETTINGS_QUERY_2025_04_21,
235 migrations::m_2025_04_21::SETTINGS_PATTERNS
236);
237define_query!(
238 SETTINGS_QUERY_2025_04_23,
239 migrations::m_2025_04_23::SETTINGS_PATTERNS
240);
241define_query!(
242 SETTINGS_QUERY_2025_05_05,
243 migrations::m_2025_05_05::SETTINGS_PATTERNS
244);
245define_query!(
246 SETTINGS_QUERY_2025_05_08,
247 migrations::m_2025_05_08::SETTINGS_PATTERNS
248);
249define_query!(
250 SETTINGS_QUERY_2025_05_29,
251 migrations::m_2025_05_29::SETTINGS_PATTERNS
252);
253define_query!(
254 SETTINGS_QUERY_2025_06_16,
255 migrations::m_2025_06_16::SETTINGS_PATTERNS
256);
257
258// custom query
259static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
260 Query::new(
261 &tree_sitter_json::LANGUAGE.into(),
262 SETTINGS_NESTED_KEY_VALUE_PATTERN,
263 )
264 .unwrap()
265});
266
267#[cfg(test)]
268mod tests {
269 use super::*;
270
271 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
272 let migrated = migrate_keymap(&input).unwrap();
273 pretty_assertions::assert_eq!(migrated.as_deref(), output);
274 }
275
276 fn assert_migrate_settings(input: &str, output: Option<&str>) {
277 let migrated = migrate_settings(&input).unwrap();
278 pretty_assertions::assert_eq!(migrated.as_deref(), output);
279 }
280
281 #[test]
282 fn test_replace_array_with_single_string() {
283 assert_migrate_keymap(
284 r#"
285 [
286 {
287 "bindings": {
288 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
289 }
290 }
291 ]
292 "#,
293 Some(
294 r#"
295 [
296 {
297 "bindings": {
298 "cmd-1": "workspace::ActivatePaneUp"
299 }
300 }
301 ]
302 "#,
303 ),
304 )
305 }
306
307 #[test]
308 fn test_replace_action_argument_object_with_single_value() {
309 assert_migrate_keymap(
310 r#"
311 [
312 {
313 "bindings": {
314 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
315 }
316 }
317 ]
318 "#,
319 Some(
320 r#"
321 [
322 {
323 "bindings": {
324 "cmd-1": ["editor::FoldAtLevel", 1]
325 }
326 }
327 ]
328 "#,
329 ),
330 )
331 }
332
333 #[test]
334 fn test_replace_action_argument_object_with_single_value_2() {
335 assert_migrate_keymap(
336 r#"
337 [
338 {
339 "bindings": {
340 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
341 }
342 }
343 ]
344 "#,
345 Some(
346 r#"
347 [
348 {
349 "bindings": {
350 "cmd-1": ["vim::PushObject", { "some" : "value" }]
351 }
352 }
353 ]
354 "#,
355 ),
356 )
357 }
358
359 #[test]
360 fn test_rename_string_action() {
361 assert_migrate_keymap(
362 r#"
363 [
364 {
365 "bindings": {
366 "cmd-1": "inline_completion::ToggleMenu"
367 }
368 }
369 ]
370 "#,
371 Some(
372 r#"
373 [
374 {
375 "bindings": {
376 "cmd-1": "edit_prediction::ToggleMenu"
377 }
378 }
379 ]
380 "#,
381 ),
382 )
383 }
384
385 #[test]
386 fn test_rename_context_key() {
387 assert_migrate_keymap(
388 r#"
389 [
390 {
391 "context": "Editor && inline_completion && !showing_completions"
392 }
393 ]
394 "#,
395 Some(
396 r#"
397 [
398 {
399 "context": "Editor && edit_prediction && !showing_completions"
400 }
401 ]
402 "#,
403 ),
404 )
405 }
406
407 #[test]
408 fn test_incremental_migrations() {
409 // Here string transforms to array internally. Then, that array transforms back to string.
410 assert_migrate_keymap(
411 r#"
412 [
413 {
414 "bindings": {
415 "ctrl-q": "editor::GoToHunk", // should remain same
416 "ctrl-w": "editor::GoToPrevHunk", // should rename
417 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
418 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
419 }
420 }
421 ]
422 "#,
423 Some(
424 r#"
425 [
426 {
427 "bindings": {
428 "ctrl-q": "editor::GoToHunk", // should remain same
429 "ctrl-w": "editor::GoToPreviousHunk", // should rename
430 "ctrl-q": "editor::GoToHunk", // should transform
431 "ctrl-w": "editor::GoToPreviousHunk" // should transform
432 }
433 }
434 ]
435 "#,
436 ),
437 )
438 }
439
440 #[test]
441 fn test_action_argument_snake_case() {
442 // First performs transformations, then replacements
443 assert_migrate_keymap(
444 r#"
445 [
446 {
447 "bindings": {
448 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
449 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
450 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
451 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
452 }
453 }
454 ]
455 "#,
456 Some(
457 r#"
458 [
459 {
460 "bindings": {
461 "cmd-1": ["vim::PushObject", { "around": false }],
462 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
463 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
464 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
465 }
466 }
467 ]
468 "#,
469 ),
470 )
471 }
472
473 #[test]
474 fn test_replace_setting_name() {
475 assert_migrate_settings(
476 r#"
477 {
478 "show_inline_completions_in_menu": true,
479 "show_inline_completions": true,
480 "inline_completions_disabled_in": ["string"],
481 "inline_completions": { "some" : "value" }
482 }
483 "#,
484 Some(
485 r#"
486 {
487 "show_edit_predictions_in_menu": true,
488 "show_edit_predictions": true,
489 "edit_predictions_disabled_in": ["string"],
490 "edit_predictions": { "some" : "value" }
491 }
492 "#,
493 ),
494 )
495 }
496
497 #[test]
498 fn test_nested_string_replace_for_settings() {
499 assert_migrate_settings(
500 r#"
501 {
502 "features": {
503 "inline_completion_provider": "zed"
504 },
505 }
506 "#,
507 Some(
508 r#"
509 {
510 "features": {
511 "edit_prediction_provider": "zed"
512 },
513 }
514 "#,
515 ),
516 )
517 }
518
519 #[test]
520 fn test_replace_settings_in_languages() {
521 assert_migrate_settings(
522 r#"
523 {
524 "languages": {
525 "Astro": {
526 "show_inline_completions": true
527 }
528 }
529 }
530 "#,
531 Some(
532 r#"
533 {
534 "languages": {
535 "Astro": {
536 "show_edit_predictions": true
537 }
538 }
539 }
540 "#,
541 ),
542 )
543 }
544
545 #[test]
546 fn test_replace_settings_value() {
547 assert_migrate_settings(
548 r#"
549 {
550 "scrollbar": {
551 "diagnostics": true
552 },
553 "chat_panel": {
554 "button": true
555 }
556 }
557 "#,
558 Some(
559 r#"
560 {
561 "scrollbar": {
562 "diagnostics": "all"
563 },
564 "chat_panel": {
565 "button": "always"
566 }
567 }
568 "#,
569 ),
570 )
571 }
572
573 #[test]
574 fn test_replace_settings_name_and_value() {
575 assert_migrate_settings(
576 r#"
577 {
578 "tabs": {
579 "always_show_close_button": true
580 }
581 }
582 "#,
583 Some(
584 r#"
585 {
586 "tabs": {
587 "show_close_button": "always"
588 }
589 }
590 "#,
591 ),
592 )
593 }
594
595 #[test]
596 fn test_replace_bash_with_terminal_in_profiles() {
597 assert_migrate_settings(
598 r#"
599 {
600 "assistant": {
601 "profiles": {
602 "custom": {
603 "name": "Custom",
604 "tools": {
605 "bash": true,
606 "diagnostics": true
607 }
608 }
609 }
610 }
611 }
612 "#,
613 Some(
614 r#"
615 {
616 "agent": {
617 "profiles": {
618 "custom": {
619 "name": "Custom",
620 "tools": {
621 "terminal": true,
622 "diagnostics": true
623 }
624 }
625 }
626 }
627 }
628 "#,
629 ),
630 )
631 }
632
633 #[test]
634 fn test_replace_bash_false_with_terminal_in_profiles() {
635 assert_migrate_settings(
636 r#"
637 {
638 "assistant": {
639 "profiles": {
640 "custom": {
641 "name": "Custom",
642 "tools": {
643 "bash": false,
644 "diagnostics": true
645 }
646 }
647 }
648 }
649 }
650 "#,
651 Some(
652 r#"
653 {
654 "agent": {
655 "profiles": {
656 "custom": {
657 "name": "Custom",
658 "tools": {
659 "terminal": false,
660 "diagnostics": true
661 }
662 }
663 }
664 }
665 }
666 "#,
667 ),
668 )
669 }
670
671 #[test]
672 fn test_no_bash_in_profiles() {
673 assert_migrate_settings(
674 r#"
675 {
676 "assistant": {
677 "profiles": {
678 "custom": {
679 "name": "Custom",
680 "tools": {
681 "diagnostics": true,
682 "find_path": true,
683 "read_file": true
684 }
685 }
686 }
687 }
688 }
689 "#,
690 Some(
691 r#"
692 {
693 "agent": {
694 "profiles": {
695 "custom": {
696 "name": "Custom",
697 "tools": {
698 "diagnostics": true,
699 "find_path": true,
700 "read_file": true
701 }
702 }
703 }
704 }
705 }
706 "#,
707 ),
708 )
709 }
710
711 #[test]
712 fn test_rename_path_search_to_find_path() {
713 assert_migrate_settings(
714 r#"
715 {
716 "assistant": {
717 "profiles": {
718 "default": {
719 "tools": {
720 "path_search": true,
721 "read_file": true
722 }
723 }
724 }
725 }
726 }
727 "#,
728 Some(
729 r#"
730 {
731 "agent": {
732 "profiles": {
733 "default": {
734 "tools": {
735 "find_path": true,
736 "read_file": true
737 }
738 }
739 }
740 }
741 }
742 "#,
743 ),
744 );
745 }
746
747 #[test]
748 fn test_rename_assistant() {
749 assert_migrate_settings(
750 r#"{
751 "assistant": {
752 "foo": "bar"
753 },
754 "edit_predictions": {
755 "enabled_in_assistant": false,
756 }
757 }"#,
758 Some(
759 r#"{
760 "agent": {
761 "foo": "bar"
762 },
763 "edit_predictions": {
764 "enabled_in_text_threads": false,
765 }
766 }"#,
767 ),
768 );
769 }
770
771 #[test]
772 fn test_comment_duplicated_agent() {
773 assert_migrate_settings(
774 r#"{
775 "agent": {
776 "name": "assistant-1",
777 "model": "gpt-4", // weird formatting
778 "utf8": "привіт"
779 },
780 "something": "else",
781 "agent": {
782 "name": "assistant-2",
783 "model": "gemini-pro"
784 }
785 }
786 "#,
787 Some(
788 r#"{
789 /* Duplicated key auto-commented: "agent": {
790 "name": "assistant-1",
791 "model": "gpt-4", // weird formatting
792 "utf8": "привіт"
793 }, */
794 "something": "else",
795 "agent": {
796 "name": "assistant-2",
797 "model": "gemini-pro"
798 }
799 }
800 "#,
801 ),
802 );
803 }
804
805 #[test]
806 fn test_preferred_completion_mode_migration() {
807 assert_migrate_settings(
808 r#"{
809 "agent": {
810 "preferred_completion_mode": "max",
811 "enabled": true
812 }
813 }"#,
814 Some(
815 r#"{
816 "agent": {
817 "preferred_completion_mode": "burn",
818 "enabled": true
819 }
820 }"#,
821 ),
822 );
823
824 assert_migrate_settings(
825 r#"{
826 "agent": {
827 "preferred_completion_mode": "normal",
828 "enabled": true
829 }
830 }"#,
831 None,
832 );
833
834 assert_migrate_settings(
835 r#"{
836 "agent": {
837 "preferred_completion_mode": "burn",
838 "enabled": true
839 }
840 }"#,
841 None,
842 );
843
844 assert_migrate_settings(
845 r#"{
846 "other_section": {
847 "preferred_completion_mode": "max"
848 },
849 "agent": {
850 "preferred_completion_mode": "max"
851 }
852 }"#,
853 Some(
854 r#"{
855 "other_section": {
856 "preferred_completion_mode": "max"
857 },
858 "agent": {
859 "preferred_completion_mode": "burn"
860 }
861 }"#,
862 ),
863 );
864 }
865
866 #[test]
867 fn test_mcp_settings_migration() {
868 assert_migrate_settings(
869 r#"{
870 "context_servers": {
871 "empty_server": {},
872 "extension_server": {
873 "settings": {
874 "foo": "bar"
875 }
876 },
877 "custom_server": {
878 "command": {
879 "path": "foo",
880 "args": ["bar"],
881 "env": {
882 "FOO": "BAR"
883 }
884 }
885 },
886 "invalid_server": {
887 "command": {
888 "path": "foo",
889 "args": ["bar"],
890 "env": {
891 "FOO": "BAR"
892 }
893 },
894 "settings": {
895 "foo": "bar"
896 }
897 },
898 "empty_server2": {},
899 "extension_server2": {
900 "foo": "bar",
901 "settings": {
902 "foo": "bar"
903 },
904 "bar": "foo"
905 },
906 "custom_server2": {
907 "foo": "bar",
908 "command": {
909 "path": "foo",
910 "args": ["bar"],
911 "env": {
912 "FOO": "BAR"
913 }
914 },
915 "bar": "foo"
916 },
917 "invalid_server2": {
918 "foo": "bar",
919 "command": {
920 "path": "foo",
921 "args": ["bar"],
922 "env": {
923 "FOO": "BAR"
924 }
925 },
926 "bar": "foo",
927 "settings": {
928 "foo": "bar"
929 }
930 }
931 }
932}"#,
933 Some(
934 r#"{
935 "context_servers": {
936 "empty_server": {
937 "source": "extension",
938 "settings": {}
939 },
940 "extension_server": {
941 "source": "extension",
942 "settings": {
943 "foo": "bar"
944 }
945 },
946 "custom_server": {
947 "source": "custom",
948 "command": {
949 "path": "foo",
950 "args": ["bar"],
951 "env": {
952 "FOO": "BAR"
953 }
954 }
955 },
956 "invalid_server": {
957 "source": "custom",
958 "command": {
959 "path": "foo",
960 "args": ["bar"],
961 "env": {
962 "FOO": "BAR"
963 }
964 },
965 "settings": {
966 "foo": "bar"
967 }
968 },
969 "empty_server2": {
970 "source": "extension",
971 "settings": {}
972 },
973 "extension_server2": {
974 "source": "extension",
975 "foo": "bar",
976 "settings": {
977 "foo": "bar"
978 },
979 "bar": "foo"
980 },
981 "custom_server2": {
982 "source": "custom",
983 "foo": "bar",
984 "command": {
985 "path": "foo",
986 "args": ["bar"],
987 "env": {
988 "FOO": "BAR"
989 }
990 },
991 "bar": "foo"
992 },
993 "invalid_server2": {
994 "source": "custom",
995 "foo": "bar",
996 "command": {
997 "path": "foo",
998 "args": ["bar"],
999 "env": {
1000 "FOO": "BAR"
1001 }
1002 },
1003 "bar": "foo",
1004 "settings": {
1005 "foo": "bar"
1006 }
1007 }
1008 }
1009}"#,
1010 ),
1011 );
1012 }
1013
1014 #[test]
1015 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1016 let settings = r#"{
1017 "context_servers": {
1018 "empty_server": {
1019 "source": "extension",
1020 "settings": {}
1021 },
1022 "extension_server": {
1023 "source": "extension",
1024 "settings": {
1025 "foo": "bar"
1026 }
1027 },
1028 "custom_server": {
1029 "source": "custom",
1030 "command": {
1031 "path": "foo",
1032 "args": ["bar"],
1033 "env": {
1034 "FOO": "BAR"
1035 }
1036 }
1037 },
1038 "invalid_server": {
1039 "source": "custom",
1040 "command": {
1041 "path": "foo",
1042 "args": ["bar"],
1043 "env": {
1044 "FOO": "BAR"
1045 }
1046 },
1047 "settings": {
1048 "foo": "bar"
1049 }
1050 }
1051 }
1052}"#;
1053 assert_migrate_settings(settings, None);
1054 }
1055}