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 ];
147 run_migrations(text, migrations)
148}
149
150enum MigrationType<'a> {
151 TreeSitter(MigrationPatterns, &'a Query),
152 Json(fn(&mut serde_json::Value) -> Result<()>),
153}
154
155pub fn migrate_settings(text: &str) -> Result<Option<String>> {
156 let migrations: &[MigrationType] = &[
157 MigrationType::TreeSitter(
158 migrations::m_2025_01_02::SETTINGS_PATTERNS,
159 &SETTINGS_QUERY_2025_01_02,
160 ),
161 MigrationType::TreeSitter(
162 migrations::m_2025_01_29::SETTINGS_PATTERNS,
163 &SETTINGS_QUERY_2025_01_29,
164 ),
165 MigrationType::TreeSitter(
166 migrations::m_2025_01_30::SETTINGS_PATTERNS,
167 &SETTINGS_QUERY_2025_01_30,
168 ),
169 MigrationType::TreeSitter(
170 migrations::m_2025_03_29::SETTINGS_PATTERNS,
171 &SETTINGS_QUERY_2025_03_29,
172 ),
173 MigrationType::TreeSitter(
174 migrations::m_2025_04_15::SETTINGS_PATTERNS,
175 &SETTINGS_QUERY_2025_04_15,
176 ),
177 MigrationType::TreeSitter(
178 migrations::m_2025_04_21::SETTINGS_PATTERNS,
179 &SETTINGS_QUERY_2025_04_21,
180 ),
181 MigrationType::TreeSitter(
182 migrations::m_2025_04_23::SETTINGS_PATTERNS,
183 &SETTINGS_QUERY_2025_04_23,
184 ),
185 MigrationType::TreeSitter(
186 migrations::m_2025_05_05::SETTINGS_PATTERNS,
187 &SETTINGS_QUERY_2025_05_05,
188 ),
189 MigrationType::TreeSitter(
190 migrations::m_2025_05_08::SETTINGS_PATTERNS,
191 &SETTINGS_QUERY_2025_05_08,
192 ),
193 MigrationType::TreeSitter(
194 migrations::m_2025_06_16::SETTINGS_PATTERNS,
195 &SETTINGS_QUERY_2025_06_16,
196 ),
197 MigrationType::TreeSitter(
198 migrations::m_2025_06_25::SETTINGS_PATTERNS,
199 &SETTINGS_QUERY_2025_06_25,
200 ),
201 MigrationType::TreeSitter(
202 migrations::m_2025_06_27::SETTINGS_PATTERNS,
203 &SETTINGS_QUERY_2025_06_27,
204 ),
205 MigrationType::TreeSitter(
206 migrations::m_2025_07_08::SETTINGS_PATTERNS,
207 &SETTINGS_QUERY_2025_07_08,
208 ),
209 MigrationType::Json(migrations::m_2025_10_01::flatten_code_actions_formatters),
210 MigrationType::Json(migrations::m_2025_10_02::remove_formatters_on_save),
211 MigrationType::TreeSitter(
212 migrations::m_2025_10_03::SETTINGS_PATTERNS,
213 &SETTINGS_QUERY_2025_10_03,
214 ),
215 MigrationType::Json(migrations::m_2025_10_16::restore_code_actions_on_format),
216 MigrationType::Json(migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum),
217 MigrationType::Json(migrations::m_2025_10_21::make_relative_line_numbers_an_enum),
218 MigrationType::TreeSitter(
219 migrations::m_2025_11_12::SETTINGS_PATTERNS,
220 &SETTINGS_QUERY_2025_11_12,
221 ),
222 MigrationType::TreeSitter(
223 migrations::m_2025_12_01::SETTINGS_PATTERNS,
224 &SETTINGS_QUERY_2025_12_01,
225 ),
226 MigrationType::TreeSitter(
227 migrations::m_2025_11_20::SETTINGS_PATTERNS,
228 &SETTINGS_QUERY_2025_11_20,
229 ),
230 MigrationType::Json(migrations::m_2025_11_25::remove_context_server_source),
231 MigrationType::TreeSitter(
232 migrations::m_2025_12_15::SETTINGS_PATTERNS,
233 &SETTINGS_QUERY_2025_12_15,
234 ),
235 MigrationType::Json(
236 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
237 ),
238 MigrationType::Json(migrations::m_2026_02_03::migrate_experimental_sweep_mercury),
239 MigrationType::Json(migrations::m_2026_02_04::migrate_tool_permission_defaults),
240 MigrationType::Json(migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry),
241 ];
242 run_migrations(text, migrations)
243}
244
245pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
246 migrate(
247 text,
248 &[(
249 SETTINGS_NESTED_KEY_VALUE_PATTERN,
250 migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
251 )],
252 &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
253 )
254}
255
256pub type MigrationPatterns = &'static [(
257 &'static str,
258 fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
259)];
260
261macro_rules! define_query {
262 ($var_name:ident, $patterns_path:path) => {
263 static $var_name: LazyLock<Query> = LazyLock::new(|| {
264 Query::new(
265 &tree_sitter_json::LANGUAGE.into(),
266 &$patterns_path
267 .iter()
268 .map(|pattern| pattern.0)
269 .collect::<String>(),
270 )
271 .unwrap()
272 });
273 };
274}
275
276// keymap
277define_query!(
278 KEYMAP_QUERY_2025_01_29,
279 migrations::m_2025_01_29::KEYMAP_PATTERNS
280);
281define_query!(
282 KEYMAP_QUERY_2025_01_30,
283 migrations::m_2025_01_30::KEYMAP_PATTERNS
284);
285define_query!(
286 KEYMAP_QUERY_2025_03_03,
287 migrations::m_2025_03_03::KEYMAP_PATTERNS
288);
289define_query!(
290 KEYMAP_QUERY_2025_03_06,
291 migrations::m_2025_03_06::KEYMAP_PATTERNS
292);
293define_query!(
294 KEYMAP_QUERY_2025_04_15,
295 migrations::m_2025_04_15::KEYMAP_PATTERNS
296);
297
298// settings
299define_query!(
300 SETTINGS_QUERY_2025_01_02,
301 migrations::m_2025_01_02::SETTINGS_PATTERNS
302);
303define_query!(
304 SETTINGS_QUERY_2025_01_29,
305 migrations::m_2025_01_29::SETTINGS_PATTERNS
306);
307define_query!(
308 SETTINGS_QUERY_2025_01_30,
309 migrations::m_2025_01_30::SETTINGS_PATTERNS
310);
311define_query!(
312 SETTINGS_QUERY_2025_03_29,
313 migrations::m_2025_03_29::SETTINGS_PATTERNS
314);
315define_query!(
316 SETTINGS_QUERY_2025_04_15,
317 migrations::m_2025_04_15::SETTINGS_PATTERNS
318);
319define_query!(
320 SETTINGS_QUERY_2025_04_21,
321 migrations::m_2025_04_21::SETTINGS_PATTERNS
322);
323define_query!(
324 SETTINGS_QUERY_2025_04_23,
325 migrations::m_2025_04_23::SETTINGS_PATTERNS
326);
327define_query!(
328 SETTINGS_QUERY_2025_05_05,
329 migrations::m_2025_05_05::SETTINGS_PATTERNS
330);
331define_query!(
332 SETTINGS_QUERY_2025_05_08,
333 migrations::m_2025_05_08::SETTINGS_PATTERNS
334);
335define_query!(
336 SETTINGS_QUERY_2025_06_16,
337 migrations::m_2025_06_16::SETTINGS_PATTERNS
338);
339define_query!(
340 SETTINGS_QUERY_2025_06_25,
341 migrations::m_2025_06_25::SETTINGS_PATTERNS
342);
343define_query!(
344 SETTINGS_QUERY_2025_06_27,
345 migrations::m_2025_06_27::SETTINGS_PATTERNS
346);
347define_query!(
348 SETTINGS_QUERY_2025_07_08,
349 migrations::m_2025_07_08::SETTINGS_PATTERNS
350);
351define_query!(
352 SETTINGS_QUERY_2025_10_03,
353 migrations::m_2025_10_03::SETTINGS_PATTERNS
354);
355define_query!(
356 SETTINGS_QUERY_2025_11_12,
357 migrations::m_2025_11_12::SETTINGS_PATTERNS
358);
359define_query!(
360 SETTINGS_QUERY_2025_12_01,
361 migrations::m_2025_12_01::SETTINGS_PATTERNS
362);
363define_query!(
364 SETTINGS_QUERY_2025_11_20,
365 migrations::m_2025_11_20::SETTINGS_PATTERNS
366);
367define_query!(
368 KEYMAP_QUERY_2025_12_08,
369 migrations::m_2025_12_08::KEYMAP_PATTERNS
370);
371define_query!(
372 SETTINGS_QUERY_2025_12_15,
373 migrations::m_2025_12_15::SETTINGS_PATTERNS
374);
375
376// custom query
377static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
378 Query::new(
379 &tree_sitter_json::LANGUAGE.into(),
380 SETTINGS_NESTED_KEY_VALUE_PATTERN,
381 )
382 .unwrap()
383});
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388 use unindent::Unindent as _;
389
390 #[track_caller]
391 fn assert_migrated_correctly(migrated: Option<String>, expected: Option<&str>) {
392 match (&migrated, &expected) {
393 (Some(migrated), Some(expected)) => {
394 pretty_assertions::assert_str_eq!(expected, migrated);
395 }
396 _ => {
397 pretty_assertions::assert_eq!(migrated.as_deref(), expected);
398 }
399 }
400 }
401
402 fn assert_migrate_keymap(input: &str, output: Option<&str>) {
403 let migrated = migrate_keymap(input).unwrap();
404 pretty_assertions::assert_eq!(migrated.as_deref(), output);
405 }
406
407 #[track_caller]
408 fn assert_migrate_settings(input: &str, output: Option<&str>) {
409 let migrated = migrate_settings(input).unwrap();
410 assert_migrated_correctly(migrated.clone(), output);
411
412 // expect that rerunning the migration does not result in another migration
413 if let Some(migrated) = migrated {
414 let rerun = migrate_settings(&migrated).unwrap();
415 assert_migrated_correctly(rerun, None);
416 }
417 }
418
419 #[track_caller]
420 fn assert_migrate_settings_with_migrations(
421 migrations: &[MigrationType],
422 input: &str,
423 output: Option<&str>,
424 ) {
425 let migrated = run_migrations(input, migrations).unwrap();
426 assert_migrated_correctly(migrated.clone(), output);
427
428 // expect that rerunning the migration does not result in another migration
429 if let Some(migrated) = migrated {
430 let rerun = run_migrations(&migrated, migrations).unwrap();
431 assert_migrated_correctly(rerun, None);
432 }
433 }
434
435 #[test]
436 fn test_empty_content() {
437 assert_migrate_settings("", None)
438 }
439
440 #[test]
441 fn test_replace_array_with_single_string() {
442 assert_migrate_keymap(
443 r#"
444 [
445 {
446 "bindings": {
447 "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
448 }
449 }
450 ]
451 "#,
452 Some(
453 r#"
454 [
455 {
456 "bindings": {
457 "cmd-1": "workspace::ActivatePaneUp"
458 }
459 }
460 ]
461 "#,
462 ),
463 )
464 }
465
466 #[test]
467 fn test_replace_action_argument_object_with_single_value() {
468 assert_migrate_keymap(
469 r#"
470 [
471 {
472 "bindings": {
473 "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
474 }
475 }
476 ]
477 "#,
478 Some(
479 r#"
480 [
481 {
482 "bindings": {
483 "cmd-1": ["editor::FoldAtLevel", 1]
484 }
485 }
486 ]
487 "#,
488 ),
489 )
490 }
491
492 #[test]
493 fn test_replace_action_argument_object_with_single_value_2() {
494 assert_migrate_keymap(
495 r#"
496 [
497 {
498 "bindings": {
499 "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
500 }
501 }
502 ]
503 "#,
504 Some(
505 r#"
506 [
507 {
508 "bindings": {
509 "cmd-1": ["vim::PushObject", { "some" : "value" }]
510 }
511 }
512 ]
513 "#,
514 ),
515 )
516 }
517
518 #[test]
519 fn test_rename_string_action() {
520 assert_migrate_keymap(
521 r#"
522 [
523 {
524 "bindings": {
525 "cmd-1": "inline_completion::ToggleMenu"
526 }
527 }
528 ]
529 "#,
530 Some(
531 r#"
532 [
533 {
534 "bindings": {
535 "cmd-1": "edit_prediction::ToggleMenu"
536 }
537 }
538 ]
539 "#,
540 ),
541 )
542 }
543
544 #[test]
545 fn test_rename_context_key() {
546 assert_migrate_keymap(
547 r#"
548 [
549 {
550 "context": "Editor && inline_completion && !showing_completions"
551 }
552 ]
553 "#,
554 Some(
555 r#"
556 [
557 {
558 "context": "Editor && edit_prediction && !showing_completions"
559 }
560 ]
561 "#,
562 ),
563 )
564 }
565
566 #[test]
567 fn test_incremental_migrations() {
568 // Here string transforms to array internally. Then, that array transforms back to string.
569 assert_migrate_keymap(
570 r#"
571 [
572 {
573 "bindings": {
574 "ctrl-q": "editor::GoToHunk", // should remain same
575 "ctrl-w": "editor::GoToPrevHunk", // should rename
576 "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
577 "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
578 }
579 }
580 ]
581 "#,
582 Some(
583 r#"
584 [
585 {
586 "bindings": {
587 "ctrl-q": "editor::GoToHunk", // should remain same
588 "ctrl-w": "editor::GoToPreviousHunk", // should rename
589 "ctrl-q": "editor::GoToHunk", // should transform
590 "ctrl-w": "editor::GoToPreviousHunk" // should transform
591 }
592 }
593 ]
594 "#,
595 ),
596 )
597 }
598
599 #[test]
600 fn test_action_argument_snake_case() {
601 // First performs transformations, then replacements
602 assert_migrate_keymap(
603 r#"
604 [
605 {
606 "bindings": {
607 "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
608 "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
609 "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
610 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
611 }
612 }
613 ]
614 "#,
615 Some(
616 r#"
617 [
618 {
619 "bindings": {
620 "cmd-1": ["vim::PushObject", { "around": false }],
621 "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
622 "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
623 "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
624 }
625 }
626 ]
627 "#,
628 ),
629 )
630 }
631
632 #[test]
633 fn test_replace_setting_name() {
634 assert_migrate_settings(
635 r#"
636 {
637 "show_inline_completions_in_menu": true,
638 "show_inline_completions": true,
639 "inline_completions_disabled_in": ["string"],
640 "inline_completions": { "some" : "value" }
641 }
642 "#,
643 Some(
644 r#"
645 {
646 "show_edit_predictions_in_menu": true,
647 "show_edit_predictions": true,
648 "edit_predictions_disabled_in": ["string"],
649 "edit_predictions": { "some" : "value" }
650 }
651 "#,
652 ),
653 )
654 }
655
656 #[test]
657 fn test_nested_string_replace_for_settings() {
658 assert_migrate_settings(
659 &r#"
660 {
661 "features": {
662 "inline_completion_provider": "zed"
663 },
664 }
665 "#
666 .unindent(),
667 Some(
668 &r#"
669 {
670 "edit_predictions": {
671 "provider": "zed"
672 }
673 }
674 "#
675 .unindent(),
676 ),
677 )
678 }
679
680 #[test]
681 fn test_replace_settings_in_languages() {
682 assert_migrate_settings(
683 r#"
684 {
685 "languages": {
686 "Astro": {
687 "show_inline_completions": true
688 }
689 }
690 }
691 "#,
692 Some(
693 r#"
694 {
695 "languages": {
696 "Astro": {
697 "show_edit_predictions": true
698 }
699 }
700 }
701 "#,
702 ),
703 )
704 }
705
706 #[test]
707 fn test_replace_settings_value() {
708 assert_migrate_settings(
709 r#"
710 {
711 "scrollbar": {
712 "diagnostics": true
713 },
714 "chat_panel": {
715 "button": true
716 }
717 }
718 "#,
719 Some(
720 r#"
721 {
722 "scrollbar": {
723 "diagnostics": "all"
724 },
725 "chat_panel": {
726 "button": "always"
727 }
728 }
729 "#,
730 ),
731 )
732 }
733
734 #[test]
735 fn test_replace_settings_name_and_value() {
736 assert_migrate_settings(
737 r#"
738 {
739 "tabs": {
740 "always_show_close_button": true
741 }
742 }
743 "#,
744 Some(
745 r#"
746 {
747 "tabs": {
748 "show_close_button": "always"
749 }
750 }
751 "#,
752 ),
753 )
754 }
755
756 #[test]
757 fn test_replace_bash_with_terminal_in_profiles() {
758 assert_migrate_settings(
759 r#"
760 {
761 "assistant": {
762 "profiles": {
763 "custom": {
764 "name": "Custom",
765 "tools": {
766 "bash": true,
767 "diagnostics": true
768 }
769 }
770 }
771 }
772 }
773 "#,
774 Some(
775 r#"
776 {
777 "agent": {
778 "profiles": {
779 "custom": {
780 "name": "Custom",
781 "tools": {
782 "terminal": true,
783 "diagnostics": true
784 }
785 }
786 }
787 }
788 }
789 "#,
790 ),
791 )
792 }
793
794 #[test]
795 fn test_replace_bash_false_with_terminal_in_profiles() {
796 assert_migrate_settings(
797 r#"
798 {
799 "assistant": {
800 "profiles": {
801 "custom": {
802 "name": "Custom",
803 "tools": {
804 "bash": false,
805 "diagnostics": true
806 }
807 }
808 }
809 }
810 }
811 "#,
812 Some(
813 r#"
814 {
815 "agent": {
816 "profiles": {
817 "custom": {
818 "name": "Custom",
819 "tools": {
820 "terminal": false,
821 "diagnostics": true
822 }
823 }
824 }
825 }
826 }
827 "#,
828 ),
829 )
830 }
831
832 #[test]
833 fn test_no_bash_in_profiles() {
834 assert_migrate_settings(
835 r#"
836 {
837 "assistant": {
838 "profiles": {
839 "custom": {
840 "name": "Custom",
841 "tools": {
842 "diagnostics": true,
843 "find_path": true,
844 "read_file": true
845 }
846 }
847 }
848 }
849 }
850 "#,
851 Some(
852 r#"
853 {
854 "agent": {
855 "profiles": {
856 "custom": {
857 "name": "Custom",
858 "tools": {
859 "diagnostics": true,
860 "find_path": true,
861 "read_file": true
862 }
863 }
864 }
865 }
866 }
867 "#,
868 ),
869 )
870 }
871
872 #[test]
873 fn test_rename_path_search_to_find_path() {
874 assert_migrate_settings(
875 r#"
876 {
877 "assistant": {
878 "profiles": {
879 "default": {
880 "tools": {
881 "path_search": true,
882 "read_file": true
883 }
884 }
885 }
886 }
887 }
888 "#,
889 Some(
890 r#"
891 {
892 "agent": {
893 "profiles": {
894 "default": {
895 "tools": {
896 "find_path": true,
897 "read_file": true
898 }
899 }
900 }
901 }
902 }
903 "#,
904 ),
905 );
906 }
907
908 #[test]
909 fn test_rename_assistant() {
910 assert_migrate_settings(
911 r#"{
912 "assistant": {
913 "foo": "bar"
914 },
915 "edit_predictions": {
916 "enabled_in_assistant": false,
917 }
918 }"#,
919 Some(
920 r#"{
921 "agent": {
922 "foo": "bar"
923 },
924 "edit_predictions": {
925 "enabled_in_text_threads": false,
926 }
927 }"#,
928 ),
929 );
930 }
931
932 #[test]
933 fn test_comment_duplicated_agent() {
934 assert_migrate_settings(
935 r#"{
936 "agent": {
937 "name": "assistant-1",
938 "model": "gpt-4", // weird formatting
939 "utf8": "привіт"
940 },
941 "something": "else",
942 "agent": {
943 "name": "assistant-2",
944 "model": "gemini-pro"
945 }
946 }
947 "#,
948 Some(
949 r#"{
950 /* Duplicated key auto-commented: "agent": {
951 "name": "assistant-1",
952 "model": "gpt-4", // weird formatting
953 "utf8": "привіт"
954 }, */
955 "something": "else",
956 "agent": {
957 "name": "assistant-2",
958 "model": "gemini-pro"
959 }
960 }
961 "#,
962 ),
963 );
964 }
965
966 #[test]
967 fn test_mcp_settings_migration() {
968 assert_migrate_settings_with_migrations(
969 &[MigrationType::TreeSitter(
970 migrations::m_2025_06_16::SETTINGS_PATTERNS,
971 &SETTINGS_QUERY_2025_06_16,
972 )],
973 r#"{
974 "context_servers": {
975 "empty_server": {},
976 "extension_server": {
977 "settings": {
978 "foo": "bar"
979 }
980 },
981 "custom_server": {
982 "command": {
983 "path": "foo",
984 "args": ["bar"],
985 "env": {
986 "FOO": "BAR"
987 }
988 }
989 },
990 "invalid_server": {
991 "command": {
992 "path": "foo",
993 "args": ["bar"],
994 "env": {
995 "FOO": "BAR"
996 }
997 },
998 "settings": {
999 "foo": "bar"
1000 }
1001 },
1002 "empty_server2": {},
1003 "extension_server2": {
1004 "foo": "bar",
1005 "settings": {
1006 "foo": "bar"
1007 },
1008 "bar": "foo"
1009 },
1010 "custom_server2": {
1011 "foo": "bar",
1012 "command": {
1013 "path": "foo",
1014 "args": ["bar"],
1015 "env": {
1016 "FOO": "BAR"
1017 }
1018 },
1019 "bar": "foo"
1020 },
1021 "invalid_server2": {
1022 "foo": "bar",
1023 "command": {
1024 "path": "foo",
1025 "args": ["bar"],
1026 "env": {
1027 "FOO": "BAR"
1028 }
1029 },
1030 "bar": "foo",
1031 "settings": {
1032 "foo": "bar"
1033 }
1034 }
1035 }
1036}"#,
1037 Some(
1038 r#"{
1039 "context_servers": {
1040 "empty_server": {
1041 "source": "extension",
1042 "settings": {}
1043 },
1044 "extension_server": {
1045 "source": "extension",
1046 "settings": {
1047 "foo": "bar"
1048 }
1049 },
1050 "custom_server": {
1051 "source": "custom",
1052 "command": {
1053 "path": "foo",
1054 "args": ["bar"],
1055 "env": {
1056 "FOO": "BAR"
1057 }
1058 }
1059 },
1060 "invalid_server": {
1061 "source": "custom",
1062 "command": {
1063 "path": "foo",
1064 "args": ["bar"],
1065 "env": {
1066 "FOO": "BAR"
1067 }
1068 },
1069 "settings": {
1070 "foo": "bar"
1071 }
1072 },
1073 "empty_server2": {
1074 "source": "extension",
1075 "settings": {}
1076 },
1077 "extension_server2": {
1078 "source": "extension",
1079 "foo": "bar",
1080 "settings": {
1081 "foo": "bar"
1082 },
1083 "bar": "foo"
1084 },
1085 "custom_server2": {
1086 "source": "custom",
1087 "foo": "bar",
1088 "command": {
1089 "path": "foo",
1090 "args": ["bar"],
1091 "env": {
1092 "FOO": "BAR"
1093 }
1094 },
1095 "bar": "foo"
1096 },
1097 "invalid_server2": {
1098 "source": "custom",
1099 "foo": "bar",
1100 "command": {
1101 "path": "foo",
1102 "args": ["bar"],
1103 "env": {
1104 "FOO": "BAR"
1105 }
1106 },
1107 "bar": "foo",
1108 "settings": {
1109 "foo": "bar"
1110 }
1111 }
1112 }
1113}"#,
1114 ),
1115 );
1116 }
1117
1118 #[test]
1119 fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1120 let settings = r#"{
1121 "context_servers": {
1122 "empty_server": {
1123 "source": "extension",
1124 "settings": {}
1125 },
1126 "extension_server": {
1127 "source": "extension",
1128 "settings": {
1129 "foo": "bar"
1130 }
1131 },
1132 "custom_server": {
1133 "source": "custom",
1134 "command": {
1135 "path": "foo",
1136 "args": ["bar"],
1137 "env": {
1138 "FOO": "BAR"
1139 }
1140 }
1141 },
1142 "invalid_server": {
1143 "source": "custom",
1144 "command": {
1145 "path": "foo",
1146 "args": ["bar"],
1147 "env": {
1148 "FOO": "BAR"
1149 }
1150 },
1151 "settings": {
1152 "foo": "bar"
1153 }
1154 }
1155 }
1156}"#;
1157 assert_migrate_settings_with_migrations(
1158 &[MigrationType::TreeSitter(
1159 migrations::m_2025_06_16::SETTINGS_PATTERNS,
1160 &SETTINGS_QUERY_2025_06_16,
1161 )],
1162 settings,
1163 None,
1164 );
1165 }
1166
1167 #[test]
1168 fn test_custom_agent_server_settings_migration() {
1169 assert_migrate_settings_with_migrations(
1170 &[MigrationType::TreeSitter(
1171 migrations::m_2025_11_20::SETTINGS_PATTERNS,
1172 &SETTINGS_QUERY_2025_11_20,
1173 )],
1174 r#"{
1175 "agent_servers": {
1176 "gemini": {
1177 "default_model": "gemini-1.5-pro"
1178 },
1179 "claude": {},
1180 "codex": {},
1181 "my-custom-agent": {
1182 "command": "/path/to/agent",
1183 "args": ["--foo"],
1184 "default_model": "my-model"
1185 },
1186 "already-migrated-agent": {
1187 "type": "custom",
1188 "command": "/path/to/agent"
1189 },
1190 "future-extension-agent": {
1191 "type": "extension",
1192 "default_model": "ext-model"
1193 }
1194 }
1195}"#,
1196 Some(
1197 r#"{
1198 "agent_servers": {
1199 "gemini": {
1200 "default_model": "gemini-1.5-pro"
1201 },
1202 "claude": {},
1203 "codex": {},
1204 "my-custom-agent": {
1205 "type": "custom",
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 ),
1221 );
1222 }
1223
1224 #[test]
1225 fn test_remove_version_fields() {
1226 assert_migrate_settings(
1227 r#"{
1228 "language_models": {
1229 "anthropic": {
1230 "version": "1",
1231 "api_url": "https://api.anthropic.com"
1232 },
1233 "openai": {
1234 "version": "1",
1235 "api_url": "https://api.openai.com/v1"
1236 }
1237 },
1238 "agent": {
1239 "version": "2",
1240 "enabled": true,
1241 "button": true,
1242 "dock": "right",
1243 "default_width": 640,
1244 "default_height": 320,
1245 "default_model": {
1246 "provider": "zed.dev",
1247 "model": "claude-sonnet-4"
1248 }
1249 }
1250}"#,
1251 Some(
1252 r#"{
1253 "language_models": {
1254 "anthropic": {
1255 "api_url": "https://api.anthropic.com"
1256 },
1257 "openai": {
1258 "api_url": "https://api.openai.com/v1"
1259 }
1260 },
1261 "agent": {
1262 "enabled": true,
1263 "button": true,
1264 "dock": "right",
1265 "default_width": 640,
1266 "default_height": 320,
1267 "default_model": {
1268 "provider": "zed.dev",
1269 "model": "claude-sonnet-4"
1270 }
1271 }
1272}"#,
1273 ),
1274 );
1275
1276 // Test that version fields in other contexts are not removed
1277 assert_migrate_settings(
1278 r#"{
1279 "language_models": {
1280 "other_provider": {
1281 "version": "1",
1282 "api_url": "https://api.example.com"
1283 }
1284 },
1285 "other_section": {
1286 "version": "1"
1287 }
1288}"#,
1289 None,
1290 );
1291 }
1292
1293 #[test]
1294 fn test_flatten_context_server_command() {
1295 assert_migrate_settings(
1296 r#"{
1297 "context_servers": {
1298 "some-mcp-server": {
1299 "command": {
1300 "path": "npx",
1301 "args": [
1302 "-y",
1303 "@supabase/mcp-server-supabase@latest",
1304 "--read-only",
1305 "--project-ref=<project-ref>"
1306 ],
1307 "env": {
1308 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1309 }
1310 }
1311 }
1312 }
1313}"#,
1314 Some(
1315 r#"{
1316 "context_servers": {
1317 "some-mcp-server": {
1318 "command": "npx",
1319 "args": [
1320 "-y",
1321 "@supabase/mcp-server-supabase@latest",
1322 "--read-only",
1323 "--project-ref=<project-ref>"
1324 ],
1325 "env": {
1326 "SUPABASE_ACCESS_TOKEN": "<personal-access-token>"
1327 }
1328 }
1329 }
1330}"#,
1331 ),
1332 );
1333
1334 // Test with additional keys in server object
1335 assert_migrate_settings(
1336 r#"{
1337 "context_servers": {
1338 "server-with-extras": {
1339 "command": {
1340 "path": "/usr/bin/node",
1341 "args": ["server.js"]
1342 },
1343 "settings": {}
1344 }
1345 }
1346}"#,
1347 Some(
1348 r#"{
1349 "context_servers": {
1350 "server-with-extras": {
1351 "command": "/usr/bin/node",
1352 "args": ["server.js"],
1353 "settings": {}
1354 }
1355 }
1356}"#,
1357 ),
1358 );
1359
1360 // Test command without args or env
1361 assert_migrate_settings(
1362 r#"{
1363 "context_servers": {
1364 "simple-server": {
1365 "command": {
1366 "path": "simple-mcp-server"
1367 }
1368 }
1369 }
1370}"#,
1371 Some(
1372 r#"{
1373 "context_servers": {
1374 "simple-server": {
1375 "command": "simple-mcp-server"
1376 }
1377 }
1378}"#,
1379 ),
1380 );
1381 }
1382
1383 #[test]
1384 fn test_flatten_code_action_formatters_basic_array() {
1385 assert_migrate_settings_with_migrations(
1386 &[MigrationType::Json(
1387 migrations::m_2025_10_01::flatten_code_actions_formatters,
1388 )],
1389 &r#"{
1390 "formatter": [
1391 {
1392 "code_actions": {
1393 "included-1": true,
1394 "included-2": true,
1395 "excluded": false,
1396 }
1397 }
1398 ]
1399 }"#
1400 .unindent(),
1401 Some(
1402 &r#"{
1403 "formatter": [
1404 {
1405 "code_action": "included-1"
1406 },
1407 {
1408 "code_action": "included-2"
1409 }
1410 ]
1411 }"#
1412 .unindent(),
1413 ),
1414 );
1415 }
1416
1417 #[test]
1418 fn test_flatten_code_action_formatters_basic_object() {
1419 assert_migrate_settings_with_migrations(
1420 &[MigrationType::Json(
1421 migrations::m_2025_10_01::flatten_code_actions_formatters,
1422 )],
1423 &r#"{
1424 "formatter": {
1425 "code_actions": {
1426 "included-1": true,
1427 "excluded": false,
1428 "included-2": true
1429 }
1430 }
1431 }"#
1432 .unindent(),
1433 Some(
1434 &r#"{
1435 "formatter": [
1436 {
1437 "code_action": "included-1"
1438 },
1439 {
1440 "code_action": "included-2"
1441 }
1442 ]
1443 }"#
1444 .unindent(),
1445 ),
1446 );
1447 }
1448
1449 #[test]
1450 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks() {
1451 assert_migrate_settings(
1452 &r#"{
1453 "formatter": [
1454 {
1455 "code_actions": {
1456 "included-1": true,
1457 "included-2": true,
1458 "excluded": false,
1459 }
1460 },
1461 {
1462 "language_server": "ruff"
1463 },
1464 {
1465 "code_actions": {
1466 "excluded": false,
1467 "excluded-2": false,
1468 }
1469 }
1470 // some comment
1471 ,
1472 {
1473 "code_actions": {
1474 "excluded": false,
1475 "included-3": true,
1476 "included-4": true,
1477 }
1478 },
1479 ]
1480 }"#
1481 .unindent(),
1482 Some(
1483 &r#"{
1484 "formatter": [
1485 {
1486 "code_action": "included-1"
1487 },
1488 {
1489 "code_action": "included-2"
1490 },
1491 {
1492 "language_server": "ruff"
1493 },
1494 {
1495 "code_action": "included-3"
1496 },
1497 {
1498 "code_action": "included-4"
1499 }
1500 ]
1501 }"#
1502 .unindent(),
1503 ),
1504 );
1505 }
1506
1507 #[test]
1508 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_languages() {
1509 assert_migrate_settings(
1510 &r#"{
1511 "languages": {
1512 "Rust": {
1513 "formatter": [
1514 {
1515 "code_actions": {
1516 "included-1": true,
1517 "included-2": true,
1518 "excluded": false,
1519 }
1520 },
1521 {
1522 "language_server": "ruff"
1523 },
1524 {
1525 "code_actions": {
1526 "excluded": false,
1527 "excluded-2": false,
1528 }
1529 }
1530 // some comment
1531 ,
1532 {
1533 "code_actions": {
1534 "excluded": false,
1535 "included-3": true,
1536 "included-4": true,
1537 }
1538 },
1539 ]
1540 }
1541 }
1542 }"#
1543 .unindent(),
1544 Some(
1545 &r#"{
1546 "languages": {
1547 "Rust": {
1548 "formatter": [
1549 {
1550 "code_action": "included-1"
1551 },
1552 {
1553 "code_action": "included-2"
1554 },
1555 {
1556 "language_server": "ruff"
1557 },
1558 {
1559 "code_action": "included-3"
1560 },
1561 {
1562 "code_action": "included-4"
1563 }
1564 ]
1565 }
1566 }
1567 }"#
1568 .unindent(),
1569 ),
1570 );
1571 }
1572
1573 #[test]
1574 fn test_flatten_code_action_formatters_array_with_multiple_action_blocks_in_defaults_and_multiple_languages()
1575 {
1576 assert_migrate_settings_with_migrations(
1577 &[MigrationType::Json(
1578 migrations::m_2025_10_01::flatten_code_actions_formatters,
1579 )],
1580 &r#"{
1581 "formatter": {
1582 "code_actions": {
1583 "default-1": true,
1584 "default-2": true,
1585 "default-3": true,
1586 "default-4": true,
1587 }
1588 },
1589 "languages": {
1590 "Rust": {
1591 "formatter": [
1592 {
1593 "code_actions": {
1594 "included-1": true,
1595 "included-2": true,
1596 "excluded": false,
1597 }
1598 },
1599 {
1600 "language_server": "ruff"
1601 },
1602 {
1603 "code_actions": {
1604 "excluded": false,
1605 "excluded-2": false,
1606 }
1607 }
1608 // some comment
1609 ,
1610 {
1611 "code_actions": {
1612 "excluded": false,
1613 "included-3": true,
1614 "included-4": true,
1615 }
1616 },
1617 ]
1618 },
1619 "Python": {
1620 "formatter": [
1621 {
1622 "language_server": "ruff"
1623 },
1624 {
1625 "code_actions": {
1626 "excluded": false,
1627 "excluded-2": false,
1628 }
1629 }
1630 // some comment
1631 ,
1632 {
1633 "code_actions": {
1634 "excluded": false,
1635 "included-3": true,
1636 "included-4": true,
1637 }
1638 },
1639 ]
1640 }
1641 }
1642 }"#
1643 .unindent(),
1644 Some(
1645 &r#"{
1646 "formatter": [
1647 {
1648 "code_action": "default-1"
1649 },
1650 {
1651 "code_action": "default-2"
1652 },
1653 {
1654 "code_action": "default-3"
1655 },
1656 {
1657 "code_action": "default-4"
1658 }
1659 ],
1660 "languages": {
1661 "Rust": {
1662 "formatter": [
1663 {
1664 "code_action": "included-1"
1665 },
1666 {
1667 "code_action": "included-2"
1668 },
1669 {
1670 "language_server": "ruff"
1671 },
1672 {
1673 "code_action": "included-3"
1674 },
1675 {
1676 "code_action": "included-4"
1677 }
1678 ]
1679 },
1680 "Python": {
1681 "formatter": [
1682 {
1683 "language_server": "ruff"
1684 },
1685 {
1686 "code_action": "included-3"
1687 },
1688 {
1689 "code_action": "included-4"
1690 }
1691 ]
1692 }
1693 }
1694 }"#
1695 .unindent(),
1696 ),
1697 );
1698 }
1699
1700 #[test]
1701 fn test_flatten_code_action_formatters_array_with_format_on_save_and_multiple_languages() {
1702 assert_migrate_settings_with_migrations(
1703 &[MigrationType::Json(
1704 migrations::m_2025_10_01::flatten_code_actions_formatters,
1705 )],
1706 &r#"{
1707 "formatter": {
1708 "code_actions": {
1709 "default-1": true,
1710 "default-2": true,
1711 "default-3": true,
1712 "default-4": true,
1713 }
1714 },
1715 "format_on_save": [
1716 {
1717 "code_actions": {
1718 "included-1": true,
1719 "included-2": true,
1720 "excluded": false,
1721 }
1722 },
1723 {
1724 "language_server": "ruff"
1725 },
1726 {
1727 "code_actions": {
1728 "excluded": false,
1729 "excluded-2": false,
1730 }
1731 }
1732 // some comment
1733 ,
1734 {
1735 "code_actions": {
1736 "excluded": false,
1737 "included-3": true,
1738 "included-4": true,
1739 }
1740 },
1741 ],
1742 "languages": {
1743 "Rust": {
1744 "format_on_save": "prettier",
1745 "formatter": [
1746 {
1747 "code_actions": {
1748 "included-1": true,
1749 "included-2": true,
1750 "excluded": false,
1751 }
1752 },
1753 {
1754 "language_server": "ruff"
1755 },
1756 {
1757 "code_actions": {
1758 "excluded": false,
1759 "excluded-2": false,
1760 }
1761 }
1762 // some comment
1763 ,
1764 {
1765 "code_actions": {
1766 "excluded": false,
1767 "included-3": true,
1768 "included-4": true,
1769 }
1770 },
1771 ]
1772 },
1773 "Python": {
1774 "format_on_save": {
1775 "code_actions": {
1776 "on-save-1": true,
1777 "on-save-2": true,
1778 }
1779 },
1780 "formatter": [
1781 {
1782 "language_server": "ruff"
1783 },
1784 {
1785 "code_actions": {
1786 "excluded": false,
1787 "excluded-2": false,
1788 }
1789 }
1790 // some comment
1791 ,
1792 {
1793 "code_actions": {
1794 "excluded": false,
1795 "included-3": true,
1796 "included-4": true,
1797 }
1798 },
1799 ]
1800 }
1801 }
1802 }"#
1803 .unindent(),
1804 Some(
1805 &r#"
1806 {
1807 "formatter": [
1808 {
1809 "code_action": "default-1"
1810 },
1811 {
1812 "code_action": "default-2"
1813 },
1814 {
1815 "code_action": "default-3"
1816 },
1817 {
1818 "code_action": "default-4"
1819 }
1820 ],
1821 "format_on_save": [
1822 {
1823 "code_action": "included-1"
1824 },
1825 {
1826 "code_action": "included-2"
1827 },
1828 {
1829 "language_server": "ruff"
1830 },
1831 {
1832 "code_action": "included-3"
1833 },
1834 {
1835 "code_action": "included-4"
1836 }
1837 ],
1838 "languages": {
1839 "Rust": {
1840 "format_on_save": "prettier",
1841 "formatter": [
1842 {
1843 "code_action": "included-1"
1844 },
1845 {
1846 "code_action": "included-2"
1847 },
1848 {
1849 "language_server": "ruff"
1850 },
1851 {
1852 "code_action": "included-3"
1853 },
1854 {
1855 "code_action": "included-4"
1856 }
1857 ]
1858 },
1859 "Python": {
1860 "format_on_save": [
1861 {
1862 "code_action": "on-save-1"
1863 },
1864 {
1865 "code_action": "on-save-2"
1866 }
1867 ],
1868 "formatter": [
1869 {
1870 "language_server": "ruff"
1871 },
1872 {
1873 "code_action": "included-3"
1874 },
1875 {
1876 "code_action": "included-4"
1877 }
1878 ]
1879 }
1880 }
1881 }"#
1882 .unindent(),
1883 ),
1884 );
1885 }
1886
1887 #[test]
1888 fn test_format_on_save_formatter_migration_basic() {
1889 assert_migrate_settings_with_migrations(
1890 &[MigrationType::Json(
1891 migrations::m_2025_10_02::remove_formatters_on_save,
1892 )],
1893 &r#"{
1894 "format_on_save": "prettier"
1895 }"#
1896 .unindent(),
1897 Some(
1898 &r#"{
1899 "formatter": "prettier",
1900 "format_on_save": "on"
1901 }"#
1902 .unindent(),
1903 ),
1904 );
1905 }
1906
1907 #[test]
1908 fn test_format_on_save_formatter_migration_array() {
1909 assert_migrate_settings_with_migrations(
1910 &[MigrationType::Json(
1911 migrations::m_2025_10_02::remove_formatters_on_save,
1912 )],
1913 &r#"{
1914 "format_on_save": ["prettier", {"language_server": "eslint"}]
1915 }"#
1916 .unindent(),
1917 Some(
1918 &r#"{
1919 "formatter": [
1920 "prettier",
1921 {
1922 "language_server": "eslint"
1923 }
1924 ],
1925 "format_on_save": "on"
1926 }"#
1927 .unindent(),
1928 ),
1929 );
1930 }
1931
1932 #[test]
1933 fn test_format_on_save_on_off_unchanged() {
1934 assert_migrate_settings_with_migrations(
1935 &[MigrationType::Json(
1936 migrations::m_2025_10_02::remove_formatters_on_save,
1937 )],
1938 &r#"{
1939 "format_on_save": "on"
1940 }"#
1941 .unindent(),
1942 None,
1943 );
1944
1945 assert_migrate_settings_with_migrations(
1946 &[MigrationType::Json(
1947 migrations::m_2025_10_02::remove_formatters_on_save,
1948 )],
1949 &r#"{
1950 "format_on_save": "off"
1951 }"#
1952 .unindent(),
1953 None,
1954 );
1955 }
1956
1957 #[test]
1958 fn test_format_on_save_formatter_migration_in_languages() {
1959 assert_migrate_settings_with_migrations(
1960 &[MigrationType::Json(
1961 migrations::m_2025_10_02::remove_formatters_on_save,
1962 )],
1963 &r#"{
1964 "languages": {
1965 "Rust": {
1966 "format_on_save": "rust-analyzer"
1967 },
1968 "Python": {
1969 "format_on_save": ["ruff", "black"]
1970 }
1971 }
1972 }"#
1973 .unindent(),
1974 Some(
1975 &r#"{
1976 "languages": {
1977 "Rust": {
1978 "formatter": "rust-analyzer",
1979 "format_on_save": "on"
1980 },
1981 "Python": {
1982 "formatter": [
1983 "ruff",
1984 "black"
1985 ],
1986 "format_on_save": "on"
1987 }
1988 }
1989 }"#
1990 .unindent(),
1991 ),
1992 );
1993 }
1994
1995 #[test]
1996 fn test_format_on_save_formatter_migration_mixed_global_and_languages() {
1997 assert_migrate_settings_with_migrations(
1998 &[MigrationType::Json(
1999 migrations::m_2025_10_02::remove_formatters_on_save,
2000 )],
2001 &r#"{
2002 "format_on_save": "prettier",
2003 "languages": {
2004 "Rust": {
2005 "format_on_save": "rust-analyzer"
2006 },
2007 "Python": {
2008 "format_on_save": "on"
2009 }
2010 }
2011 }"#
2012 .unindent(),
2013 Some(
2014 &r#"{
2015 "formatter": "prettier",
2016 "format_on_save": "on",
2017 "languages": {
2018 "Rust": {
2019 "formatter": "rust-analyzer",
2020 "format_on_save": "on"
2021 },
2022 "Python": {
2023 "format_on_save": "on"
2024 }
2025 }
2026 }"#
2027 .unindent(),
2028 ),
2029 );
2030 }
2031
2032 #[test]
2033 fn test_format_on_save_no_migration_when_no_format_on_save() {
2034 assert_migrate_settings_with_migrations(
2035 &[MigrationType::Json(
2036 migrations::m_2025_10_02::remove_formatters_on_save,
2037 )],
2038 &r#"{
2039 "formatter": ["prettier"]
2040 }"#
2041 .unindent(),
2042 None,
2043 );
2044 }
2045
2046 #[test]
2047 fn test_restore_code_actions_on_format() {
2048 assert_migrate_settings_with_migrations(
2049 &[MigrationType::Json(
2050 migrations::m_2025_10_16::restore_code_actions_on_format,
2051 )],
2052 &r#"{
2053 "formatter": {
2054 "code_action": "foo"
2055 }
2056 }"#
2057 .unindent(),
2058 Some(
2059 &r#"{
2060 "code_actions_on_format": {
2061 "foo": true
2062 },
2063 "formatter": []
2064 }"#
2065 .unindent(),
2066 ),
2067 );
2068
2069 assert_migrate_settings_with_migrations(
2070 &[MigrationType::Json(
2071 migrations::m_2025_10_16::restore_code_actions_on_format,
2072 )],
2073 &r#"{
2074 "formatter": [
2075 { "code_action": "foo" },
2076 "auto"
2077 ]
2078 }"#
2079 .unindent(),
2080 None,
2081 );
2082
2083 assert_migrate_settings_with_migrations(
2084 &[MigrationType::Json(
2085 migrations::m_2025_10_16::restore_code_actions_on_format,
2086 )],
2087 &r#"{
2088 "formatter": {
2089 "code_action": "foo"
2090 },
2091 "code_actions_on_format": {
2092 "bar": true,
2093 "baz": false
2094 }
2095 }"#
2096 .unindent(),
2097 Some(
2098 &r#"{
2099 "formatter": [],
2100 "code_actions_on_format": {
2101 "foo": true,
2102 "bar": true,
2103 "baz": false
2104 }
2105 }"#
2106 .unindent(),
2107 ),
2108 );
2109
2110 assert_migrate_settings_with_migrations(
2111 &[MigrationType::Json(
2112 migrations::m_2025_10_16::restore_code_actions_on_format,
2113 )],
2114 &r#"{
2115 "formatter": [
2116 { "code_action": "foo" },
2117 { "code_action": "qux" },
2118 ],
2119 "code_actions_on_format": {
2120 "bar": true,
2121 "baz": false
2122 }
2123 }"#
2124 .unindent(),
2125 Some(
2126 &r#"{
2127 "formatter": [],
2128 "code_actions_on_format": {
2129 "foo": true,
2130 "qux": true,
2131 "bar": true,
2132 "baz": false
2133 }
2134 }"#
2135 .unindent(),
2136 ),
2137 );
2138
2139 assert_migrate_settings_with_migrations(
2140 &[MigrationType::Json(
2141 migrations::m_2025_10_16::restore_code_actions_on_format,
2142 )],
2143 &r#"{
2144 "formatter": [],
2145 "code_actions_on_format": {
2146 "bar": true,
2147 "baz": false
2148 }
2149 }"#
2150 .unindent(),
2151 None,
2152 );
2153 }
2154
2155 #[test]
2156 fn test_make_file_finder_include_ignored_an_enum() {
2157 assert_migrate_settings_with_migrations(
2158 &[MigrationType::Json(
2159 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2160 )],
2161 &r#"{ }"#.unindent(),
2162 None,
2163 );
2164
2165 assert_migrate_settings_with_migrations(
2166 &[MigrationType::Json(
2167 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2168 )],
2169 &r#"{
2170 "file_finder": {
2171 "include_ignored": true
2172 }
2173 }"#
2174 .unindent(),
2175 Some(
2176 &r#"{
2177 "file_finder": {
2178 "include_ignored": "all"
2179 }
2180 }"#
2181 .unindent(),
2182 ),
2183 );
2184
2185 assert_migrate_settings_with_migrations(
2186 &[MigrationType::Json(
2187 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2188 )],
2189 &r#"{
2190 "file_finder": {
2191 "include_ignored": false
2192 }
2193 }"#
2194 .unindent(),
2195 Some(
2196 &r#"{
2197 "file_finder": {
2198 "include_ignored": "indexed"
2199 }
2200 }"#
2201 .unindent(),
2202 ),
2203 );
2204
2205 assert_migrate_settings_with_migrations(
2206 &[MigrationType::Json(
2207 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2208 )],
2209 &r#"{
2210 "file_finder": {
2211 "include_ignored": null
2212 }
2213 }"#
2214 .unindent(),
2215 Some(
2216 &r#"{
2217 "file_finder": {
2218 "include_ignored": "smart"
2219 }
2220 }"#
2221 .unindent(),
2222 ),
2223 );
2224
2225 // Platform key: settings nested inside "linux" should be migrated
2226 assert_migrate_settings_with_migrations(
2227 &[MigrationType::Json(
2228 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2229 )],
2230 &r#"
2231 {
2232 "linux": {
2233 "file_finder": {
2234 "include_ignored": true
2235 }
2236 }
2237 }
2238 "#
2239 .unindent(),
2240 Some(
2241 &r#"
2242 {
2243 "linux": {
2244 "file_finder": {
2245 "include_ignored": "all"
2246 }
2247 }
2248 }
2249 "#
2250 .unindent(),
2251 ),
2252 );
2253
2254 // Profile: settings nested inside profiles should be migrated
2255 assert_migrate_settings_with_migrations(
2256 &[MigrationType::Json(
2257 migrations::m_2025_10_17::make_file_finder_include_ignored_an_enum,
2258 )],
2259 &r#"
2260 {
2261 "profiles": {
2262 "work": {
2263 "file_finder": {
2264 "include_ignored": false
2265 }
2266 }
2267 }
2268 }
2269 "#
2270 .unindent(),
2271 Some(
2272 &r#"
2273 {
2274 "profiles": {
2275 "work": {
2276 "file_finder": {
2277 "include_ignored": "indexed"
2278 }
2279 }
2280 }
2281 }
2282 "#
2283 .unindent(),
2284 ),
2285 );
2286 }
2287
2288 #[test]
2289 fn test_make_relative_line_numbers_an_enum() {
2290 assert_migrate_settings_with_migrations(
2291 &[MigrationType::Json(
2292 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2293 )],
2294 &r#"{ }"#.unindent(),
2295 None,
2296 );
2297
2298 assert_migrate_settings_with_migrations(
2299 &[MigrationType::Json(
2300 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2301 )],
2302 &r#"{
2303 "relative_line_numbers": true
2304 }"#
2305 .unindent(),
2306 Some(
2307 &r#"{
2308 "relative_line_numbers": "enabled"
2309 }"#
2310 .unindent(),
2311 ),
2312 );
2313
2314 assert_migrate_settings_with_migrations(
2315 &[MigrationType::Json(
2316 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2317 )],
2318 &r#"{
2319 "relative_line_numbers": false
2320 }"#
2321 .unindent(),
2322 Some(
2323 &r#"{
2324 "relative_line_numbers": "disabled"
2325 }"#
2326 .unindent(),
2327 ),
2328 );
2329
2330 // Platform key: settings nested inside "macos" should be migrated
2331 assert_migrate_settings_with_migrations(
2332 &[MigrationType::Json(
2333 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2334 )],
2335 &r#"
2336 {
2337 "macos": {
2338 "relative_line_numbers": true
2339 }
2340 }
2341 "#
2342 .unindent(),
2343 Some(
2344 &r#"
2345 {
2346 "macos": {
2347 "relative_line_numbers": "enabled"
2348 }
2349 }
2350 "#
2351 .unindent(),
2352 ),
2353 );
2354
2355 // Profile: settings nested inside profiles should be migrated
2356 assert_migrate_settings_with_migrations(
2357 &[MigrationType::Json(
2358 migrations::m_2025_10_21::make_relative_line_numbers_an_enum,
2359 )],
2360 &r#"
2361 {
2362 "profiles": {
2363 "dev": {
2364 "relative_line_numbers": false
2365 }
2366 }
2367 }
2368 "#
2369 .unindent(),
2370 Some(
2371 &r#"
2372 {
2373 "profiles": {
2374 "dev": {
2375 "relative_line_numbers": "disabled"
2376 }
2377 }
2378 }
2379 "#
2380 .unindent(),
2381 ),
2382 );
2383 }
2384
2385 #[test]
2386 fn test_remove_context_server_source() {
2387 assert_migrate_settings(
2388 &r#"
2389 {
2390 "context_servers": {
2391 "extension_server": {
2392 "source": "extension",
2393 "settings": {
2394 "foo": "bar"
2395 }
2396 },
2397 "custom_server": {
2398 "source": "custom",
2399 "command": "foo",
2400 "args": ["bar"],
2401 "env": {
2402 "FOO": "BAR"
2403 }
2404 },
2405 }
2406 }
2407 "#
2408 .unindent(),
2409 Some(
2410 &r#"
2411 {
2412 "context_servers": {
2413 "extension_server": {
2414 "settings": {
2415 "foo": "bar"
2416 }
2417 },
2418 "custom_server": {
2419 "command": "foo",
2420 "args": ["bar"],
2421 "env": {
2422 "FOO": "BAR"
2423 }
2424 },
2425 }
2426 }
2427 "#
2428 .unindent(),
2429 ),
2430 );
2431
2432 // Platform key: settings nested inside "linux" should be migrated
2433 assert_migrate_settings_with_migrations(
2434 &[MigrationType::Json(
2435 migrations::m_2025_11_25::remove_context_server_source,
2436 )],
2437 &r#"
2438 {
2439 "linux": {
2440 "context_servers": {
2441 "my_server": {
2442 "source": "extension",
2443 "settings": {
2444 "key": "value"
2445 }
2446 }
2447 }
2448 }
2449 }
2450 "#
2451 .unindent(),
2452 Some(
2453 &r#"
2454 {
2455 "linux": {
2456 "context_servers": {
2457 "my_server": {
2458 "settings": {
2459 "key": "value"
2460 }
2461 }
2462 }
2463 }
2464 }
2465 "#
2466 .unindent(),
2467 ),
2468 );
2469
2470 // Profile: settings nested inside profiles should be migrated
2471 assert_migrate_settings_with_migrations(
2472 &[MigrationType::Json(
2473 migrations::m_2025_11_25::remove_context_server_source,
2474 )],
2475 &r#"
2476 {
2477 "profiles": {
2478 "work": {
2479 "context_servers": {
2480 "my_server": {
2481 "source": "custom",
2482 "command": "foo",
2483 "args": ["bar"]
2484 }
2485 }
2486 }
2487 }
2488 }
2489 "#
2490 .unindent(),
2491 Some(
2492 &r#"
2493 {
2494 "profiles": {
2495 "work": {
2496 "context_servers": {
2497 "my_server": {
2498 "command": "foo",
2499 "args": ["bar"]
2500 }
2501 }
2502 }
2503 }
2504 }
2505 "#
2506 .unindent(),
2507 ),
2508 );
2509 }
2510
2511 #[test]
2512 fn test_project_panel_open_file_on_paste_migration() {
2513 assert_migrate_settings(
2514 &r#"
2515 {
2516 "project_panel": {
2517 "open_file_on_paste": true
2518 }
2519 }
2520 "#
2521 .unindent(),
2522 Some(
2523 &r#"
2524 {
2525 "project_panel": {
2526 "auto_open": { "on_paste": true }
2527 }
2528 }
2529 "#
2530 .unindent(),
2531 ),
2532 );
2533
2534 assert_migrate_settings(
2535 &r#"
2536 {
2537 "project_panel": {
2538 "open_file_on_paste": false
2539 }
2540 }
2541 "#
2542 .unindent(),
2543 Some(
2544 &r#"
2545 {
2546 "project_panel": {
2547 "auto_open": { "on_paste": false }
2548 }
2549 }
2550 "#
2551 .unindent(),
2552 ),
2553 );
2554 }
2555
2556 #[test]
2557 fn test_enable_preview_from_code_navigation_migration() {
2558 assert_migrate_settings(
2559 &r#"
2560 {
2561 "other_setting_1": 1,
2562 "preview_tabs": {
2563 "other_setting_2": 2,
2564 "enable_preview_from_code_navigation": false
2565 }
2566 }
2567 "#
2568 .unindent(),
2569 Some(
2570 &r#"
2571 {
2572 "other_setting_1": 1,
2573 "preview_tabs": {
2574 "other_setting_2": 2,
2575 "enable_keep_preview_on_code_navigation": false
2576 }
2577 }
2578 "#
2579 .unindent(),
2580 ),
2581 );
2582
2583 assert_migrate_settings(
2584 &r#"
2585 {
2586 "other_setting_1": 1,
2587 "preview_tabs": {
2588 "other_setting_2": 2,
2589 "enable_preview_from_code_navigation": true
2590 }
2591 }
2592 "#
2593 .unindent(),
2594 Some(
2595 &r#"
2596 {
2597 "other_setting_1": 1,
2598 "preview_tabs": {
2599 "other_setting_2": 2,
2600 "enable_keep_preview_on_code_navigation": true
2601 }
2602 }
2603 "#
2604 .unindent(),
2605 ),
2606 );
2607 }
2608
2609 #[test]
2610 fn test_move_edit_prediction_provider_to_edit_predictions() {
2611 assert_migrate_settings_with_migrations(
2612 &[MigrationType::Json(
2613 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2614 )],
2615 &r#"{ }"#.unindent(),
2616 None,
2617 );
2618
2619 assert_migrate_settings_with_migrations(
2620 &[MigrationType::Json(
2621 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2622 )],
2623 &r#"
2624 {
2625 "features": {
2626 "edit_prediction_provider": "copilot"
2627 }
2628 }
2629 "#
2630 .unindent(),
2631 Some(
2632 &r#"
2633 {
2634 "edit_predictions": {
2635 "provider": "copilot"
2636 }
2637 }
2638 "#
2639 .unindent(),
2640 ),
2641 );
2642
2643 assert_migrate_settings_with_migrations(
2644 &[MigrationType::Json(
2645 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2646 )],
2647 &r#"
2648 {
2649 "features": {
2650 "edit_prediction_provider": "zed"
2651 },
2652 "edit_predictions": {
2653 "mode": "eager"
2654 }
2655 }
2656 "#
2657 .unindent(),
2658 Some(
2659 &r#"
2660 {
2661 "edit_predictions": {
2662 "provider": "zed",
2663 "mode": "eager"
2664 }
2665 }
2666 "#
2667 .unindent(),
2668 ),
2669 );
2670
2671 assert_migrate_settings_with_migrations(
2672 &[MigrationType::Json(
2673 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2674 )],
2675 &r#"
2676 {
2677 "features": {
2678 "edit_prediction_provider": "supermaven"
2679 },
2680 "edit_predictions": {
2681 "provider": "copilot"
2682 }
2683 }
2684 "#
2685 .unindent(),
2686 Some(
2687 &r#"
2688 {
2689 "edit_predictions": {
2690 "provider": "copilot"
2691 }
2692 }
2693 "#
2694 .unindent(),
2695 ),
2696 );
2697
2698 assert_migrate_settings_with_migrations(
2699 &[MigrationType::Json(
2700 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2701 )],
2702 &r#"
2703 {
2704 "edit_predictions": {
2705 "provider": "zed"
2706 }
2707 }
2708 "#
2709 .unindent(),
2710 None,
2711 );
2712
2713 // Non-object edit_predictions (e.g. true) should gracefully skip
2714 // instead of bail!-ing and aborting the entire migration chain.
2715 assert_migrate_settings_with_migrations(
2716 &[MigrationType::Json(
2717 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2718 )],
2719 &r#"
2720 {
2721 "features": {
2722 "edit_prediction_provider": "copilot"
2723 },
2724 "edit_predictions": true
2725 }
2726 "#
2727 .unindent(),
2728 Some(
2729 &r#"
2730 {
2731 "edit_predictions": true
2732 }
2733 "#
2734 .unindent(),
2735 ),
2736 );
2737
2738 // Platform key: settings nested inside "macos" should be migrated
2739 assert_migrate_settings_with_migrations(
2740 &[MigrationType::Json(
2741 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2742 )],
2743 &r#"
2744 {
2745 "macos": {
2746 "features": {
2747 "edit_prediction_provider": "copilot"
2748 }
2749 }
2750 }
2751 "#
2752 .unindent(),
2753 Some(
2754 &r#"
2755 {
2756 "macos": {
2757 "edit_predictions": {
2758 "provider": "copilot"
2759 }
2760 }
2761 }
2762 "#
2763 .unindent(),
2764 ),
2765 );
2766
2767 // Profile: settings nested inside profiles should be migrated
2768 assert_migrate_settings_with_migrations(
2769 &[MigrationType::Json(
2770 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2771 )],
2772 &r#"
2773 {
2774 "profiles": {
2775 "work": {
2776 "features": {
2777 "edit_prediction_provider": "copilot"
2778 }
2779 }
2780 }
2781 }
2782 "#
2783 .unindent(),
2784 Some(
2785 &r#"
2786 {
2787 "profiles": {
2788 "work": {
2789 "edit_predictions": {
2790 "provider": "copilot"
2791 }
2792 }
2793 }
2794 }
2795 "#
2796 .unindent(),
2797 ),
2798 );
2799
2800 // Combined: root + platform + profile should all be migrated simultaneously
2801 assert_migrate_settings_with_migrations(
2802 &[MigrationType::Json(
2803 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2804 )],
2805 &r#"
2806 {
2807 "features": {
2808 "edit_prediction_provider": "copilot"
2809 },
2810 "macos": {
2811 "features": {
2812 "edit_prediction_provider": "zed"
2813 }
2814 },
2815 "profiles": {
2816 "work": {
2817 "features": {
2818 "edit_prediction_provider": "supermaven"
2819 }
2820 }
2821 }
2822 }
2823 "#
2824 .unindent(),
2825 Some(
2826 &r#"
2827 {
2828 "edit_predictions": {
2829 "provider": "copilot"
2830 },
2831 "macos": {
2832 "edit_predictions": {
2833 "provider": "zed"
2834 }
2835 },
2836 "profiles": {
2837 "work": {
2838 "edit_predictions": {
2839 "provider": "supermaven"
2840 }
2841 }
2842 }
2843 }
2844 "#
2845 .unindent(),
2846 ),
2847 );
2848 }
2849
2850 #[test]
2851 fn test_migrate_experimental_sweep_mercury() {
2852 assert_migrate_settings_with_migrations(
2853 &[MigrationType::Json(
2854 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2855 )],
2856 &r#"{ }"#.unindent(),
2857 None,
2858 );
2859
2860 assert_migrate_settings_with_migrations(
2861 &[MigrationType::Json(
2862 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2863 )],
2864 &r#"
2865 {
2866 "edit_predictions": {
2867 "provider": {
2868 "experimental": "sweep"
2869 }
2870 }
2871 }
2872 "#
2873 .unindent(),
2874 Some(
2875 &r#"
2876 {
2877 "edit_predictions": {
2878 "provider": "sweep"
2879 }
2880 }
2881 "#
2882 .unindent(),
2883 ),
2884 );
2885
2886 assert_migrate_settings_with_migrations(
2887 &[MigrationType::Json(
2888 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2889 )],
2890 &r#"
2891 {
2892 "edit_predictions": {
2893 "provider": {
2894 "experimental": "mercury"
2895 }
2896 }
2897 }
2898 "#
2899 .unindent(),
2900 Some(
2901 &r#"
2902 {
2903 "edit_predictions": {
2904 "provider": "mercury"
2905 }
2906 }
2907 "#
2908 .unindent(),
2909 ),
2910 );
2911
2912 assert_migrate_settings_with_migrations(
2913 &[MigrationType::Json(
2914 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2915 )],
2916 &r#"
2917 {
2918 "features": {
2919 "edit_prediction_provider": {
2920 "experimental": "sweep"
2921 }
2922 }
2923 }
2924 "#
2925 .unindent(),
2926 Some(
2927 &r#"
2928 {
2929 "features": {
2930 "edit_prediction_provider": "sweep"
2931 }
2932 }
2933 "#
2934 .unindent(),
2935 ),
2936 );
2937
2938 assert_migrate_settings_with_migrations(
2939 &[MigrationType::Json(
2940 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2941 )],
2942 &r#"
2943 {
2944 "edit_predictions": {
2945 "provider": "zed"
2946 }
2947 }
2948 "#
2949 .unindent(),
2950 None,
2951 );
2952
2953 assert_migrate_settings_with_migrations(
2954 &[MigrationType::Json(
2955 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2956 )],
2957 &r#"
2958 {
2959 "edit_predictions": {
2960 "provider": {
2961 "experimental": "zeta2"
2962 }
2963 }
2964 }
2965 "#
2966 .unindent(),
2967 None,
2968 );
2969
2970 // Platform key: settings nested inside "linux" should be migrated
2971 assert_migrate_settings_with_migrations(
2972 &[MigrationType::Json(
2973 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
2974 )],
2975 &r#"
2976 {
2977 "linux": {
2978 "edit_predictions": {
2979 "provider": {
2980 "experimental": "sweep"
2981 }
2982 }
2983 }
2984 }
2985 "#
2986 .unindent(),
2987 Some(
2988 &r#"
2989 {
2990 "linux": {
2991 "edit_predictions": {
2992 "provider": "sweep"
2993 }
2994 }
2995 }
2996 "#
2997 .unindent(),
2998 ),
2999 );
3000
3001 // Profile: settings nested inside profiles should be migrated
3002 assert_migrate_settings_with_migrations(
3003 &[MigrationType::Json(
3004 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3005 )],
3006 &r#"
3007 {
3008 "profiles": {
3009 "dev": {
3010 "edit_predictions": {
3011 "provider": {
3012 "experimental": "mercury"
3013 }
3014 }
3015 }
3016 }
3017 }
3018 "#
3019 .unindent(),
3020 Some(
3021 &r#"
3022 {
3023 "profiles": {
3024 "dev": {
3025 "edit_predictions": {
3026 "provider": "mercury"
3027 }
3028 }
3029 }
3030 }
3031 "#
3032 .unindent(),
3033 ),
3034 );
3035
3036 // Combined: root + platform + profile should all be migrated simultaneously
3037 assert_migrate_settings_with_migrations(
3038 &[MigrationType::Json(
3039 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3040 )],
3041 &r#"
3042 {
3043 "edit_predictions": {
3044 "provider": {
3045 "experimental": "sweep"
3046 }
3047 },
3048 "linux": {
3049 "edit_predictions": {
3050 "provider": {
3051 "experimental": "mercury"
3052 }
3053 }
3054 },
3055 "profiles": {
3056 "dev": {
3057 "edit_predictions": {
3058 "provider": {
3059 "experimental": "sweep"
3060 }
3061 }
3062 }
3063 }
3064 }
3065 "#
3066 .unindent(),
3067 Some(
3068 &r#"
3069 {
3070 "edit_predictions": {
3071 "provider": "sweep"
3072 },
3073 "linux": {
3074 "edit_predictions": {
3075 "provider": "mercury"
3076 }
3077 },
3078 "profiles": {
3079 "dev": {
3080 "edit_predictions": {
3081 "provider": "sweep"
3082 }
3083 }
3084 }
3085 }
3086 "#
3087 .unindent(),
3088 ),
3089 );
3090 }
3091
3092 #[test]
3093 fn test_migrate_always_allow_tool_actions_to_default() {
3094 // No agent settings - no change
3095 assert_migrate_settings_with_migrations(
3096 &[MigrationType::Json(
3097 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3098 )],
3099 &r#"{ }"#.unindent(),
3100 None,
3101 );
3102
3103 // always_allow_tool_actions: true -> tool_permissions.default: "allow"
3104 assert_migrate_settings_with_migrations(
3105 &[MigrationType::Json(
3106 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3107 )],
3108 &r#"
3109 {
3110 "agent": {
3111 "always_allow_tool_actions": true
3112 }
3113 }
3114 "#
3115 .unindent(),
3116 Some(
3117 &r#"
3118 {
3119 "agent": {
3120 "tool_permissions": {
3121 "default": "allow"
3122 }
3123 }
3124 }
3125 "#
3126 .unindent(),
3127 ),
3128 );
3129
3130 // always_allow_tool_actions: false -> just remove it
3131 assert_migrate_settings_with_migrations(
3132 &[MigrationType::Json(
3133 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3134 )],
3135 &r#"
3136 {
3137 "agent": {
3138 "always_allow_tool_actions": false
3139 }
3140 }
3141 "#
3142 .unindent(),
3143 Some(
3144 // The blank line has spaces because the migration preserves the original indentation
3145 "{\n \"agent\": {\n \n }\n}\n",
3146 ),
3147 );
3148
3149 // Preserve existing tool_permissions.tools when migrating
3150 assert_migrate_settings_with_migrations(
3151 &[MigrationType::Json(
3152 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3153 )],
3154 &r#"
3155 {
3156 "agent": {
3157 "always_allow_tool_actions": true,
3158 "tool_permissions": {
3159 "tools": {
3160 "terminal": {
3161 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3162 }
3163 }
3164 }
3165 }
3166 }
3167 "#
3168 .unindent(),
3169 Some(
3170 &r#"
3171 {
3172 "agent": {
3173 "tool_permissions": {
3174 "default": "allow",
3175 "tools": {
3176 "terminal": {
3177 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3178 }
3179 }
3180 }
3181 }
3182 }
3183 "#
3184 .unindent(),
3185 ),
3186 );
3187
3188 // Don't override existing default (and migrate default_mode to default)
3189 assert_migrate_settings_with_migrations(
3190 &[MigrationType::Json(
3191 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3192 )],
3193 &r#"
3194 {
3195 "agent": {
3196 "always_allow_tool_actions": true,
3197 "tool_permissions": {
3198 "default_mode": "confirm"
3199 }
3200 }
3201 }
3202 "#
3203 .unindent(),
3204 Some(
3205 &r#"
3206 {
3207 "agent": {
3208 "tool_permissions": {
3209 "default": "confirm"
3210 }
3211 }
3212 }
3213 "#
3214 .unindent(),
3215 ),
3216 );
3217
3218 // Migrate existing default_mode to default (no always_allow_tool_actions)
3219 assert_migrate_settings_with_migrations(
3220 &[MigrationType::Json(
3221 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3222 )],
3223 &r#"
3224 {
3225 "agent": {
3226 "tool_permissions": {
3227 "default_mode": "allow"
3228 }
3229 }
3230 }
3231 "#
3232 .unindent(),
3233 Some(
3234 &r#"
3235 {
3236 "agent": {
3237 "tool_permissions": {
3238 "default": "allow"
3239 }
3240 }
3241 }
3242 "#
3243 .unindent(),
3244 ),
3245 );
3246
3247 // No migration needed if already using new format with "default"
3248 assert_migrate_settings_with_migrations(
3249 &[MigrationType::Json(
3250 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3251 )],
3252 &r#"
3253 {
3254 "agent": {
3255 "tool_permissions": {
3256 "default": "allow"
3257 }
3258 }
3259 }
3260 "#
3261 .unindent(),
3262 None,
3263 );
3264
3265 // Migrate default_mode to default in tool-specific rules
3266 assert_migrate_settings_with_migrations(
3267 &[MigrationType::Json(
3268 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3269 )],
3270 &r#"
3271 {
3272 "agent": {
3273 "tool_permissions": {
3274 "default_mode": "confirm",
3275 "tools": {
3276 "terminal": {
3277 "default_mode": "allow"
3278 }
3279 }
3280 }
3281 }
3282 }
3283 "#
3284 .unindent(),
3285 Some(
3286 &r#"
3287 {
3288 "agent": {
3289 "tool_permissions": {
3290 "default": "confirm",
3291 "tools": {
3292 "terminal": {
3293 "default": "allow"
3294 }
3295 }
3296 }
3297 }
3298 }
3299 "#
3300 .unindent(),
3301 ),
3302 );
3303
3304 // When tool_permissions is null, replace it so always_allow is preserved
3305 assert_migrate_settings_with_migrations(
3306 &[MigrationType::Json(
3307 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3308 )],
3309 &r#"
3310 {
3311 "agent": {
3312 "always_allow_tool_actions": true,
3313 "tool_permissions": null
3314 }
3315 }
3316 "#
3317 .unindent(),
3318 Some(
3319 &r#"
3320 {
3321 "agent": {
3322 "tool_permissions": {
3323 "default": "allow"
3324 }
3325 }
3326 }
3327 "#
3328 .unindent(),
3329 ),
3330 );
3331
3332 // Platform-specific agent migration
3333 assert_migrate_settings_with_migrations(
3334 &[MigrationType::Json(
3335 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3336 )],
3337 &r#"
3338 {
3339 "linux": {
3340 "agent": {
3341 "always_allow_tool_actions": true
3342 }
3343 }
3344 }
3345 "#
3346 .unindent(),
3347 Some(
3348 &r#"
3349 {
3350 "linux": {
3351 "agent": {
3352 "tool_permissions": {
3353 "default": "allow"
3354 }
3355 }
3356 }
3357 }
3358 "#
3359 .unindent(),
3360 ),
3361 );
3362
3363 // Channel-specific agent migration
3364 assert_migrate_settings_with_migrations(
3365 &[MigrationType::Json(
3366 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3367 )],
3368 &r#"
3369 {
3370 "agent": {
3371 "always_allow_tool_actions": true
3372 },
3373 "nightly": {
3374 "agent": {
3375 "tool_permissions": {
3376 "default_mode": "confirm"
3377 }
3378 }
3379 }
3380 }
3381 "#
3382 .unindent(),
3383 Some(
3384 &r#"
3385 {
3386 "agent": {
3387 "tool_permissions": {
3388 "default": "allow"
3389 }
3390 },
3391 "nightly": {
3392 "agent": {
3393 "tool_permissions": {
3394 "default": "confirm"
3395 }
3396 }
3397 }
3398 }
3399 "#
3400 .unindent(),
3401 ),
3402 );
3403
3404 // Profile-level migration
3405 assert_migrate_settings_with_migrations(
3406 &[MigrationType::Json(
3407 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3408 )],
3409 &r#"
3410 {
3411 "agent": {
3412 "profiles": {
3413 "custom": {
3414 "always_allow_tool_actions": true,
3415 "tool_permissions": {
3416 "default_mode": "allow"
3417 }
3418 }
3419 }
3420 }
3421 }
3422 "#
3423 .unindent(),
3424 Some(
3425 &r#"
3426 {
3427 "agent": {
3428 "profiles": {
3429 "custom": {
3430 "tool_permissions": {
3431 "default": "allow"
3432 }
3433 }
3434 }
3435 }
3436 }
3437 "#
3438 .unindent(),
3439 ),
3440 );
3441
3442 // Platform-specific agent with profiles
3443 assert_migrate_settings_with_migrations(
3444 &[MigrationType::Json(
3445 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3446 )],
3447 &r#"
3448 {
3449 "macos": {
3450 "agent": {
3451 "always_allow_tool_actions": true,
3452 "profiles": {
3453 "strict": {
3454 "tool_permissions": {
3455 "default_mode": "deny"
3456 }
3457 }
3458 }
3459 }
3460 }
3461 }
3462 "#
3463 .unindent(),
3464 Some(
3465 &r#"
3466 {
3467 "macos": {
3468 "agent": {
3469 "tool_permissions": {
3470 "default": "allow"
3471 },
3472 "profiles": {
3473 "strict": {
3474 "tool_permissions": {
3475 "default": "deny"
3476 }
3477 }
3478 }
3479 }
3480 }
3481 }
3482 "#
3483 .unindent(),
3484 ),
3485 );
3486
3487 // Root-level profile with always_allow_tool_actions
3488 assert_migrate_settings_with_migrations(
3489 &[MigrationType::Json(
3490 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3491 )],
3492 &r#"
3493 {
3494 "profiles": {
3495 "work": {
3496 "agent": {
3497 "always_allow_tool_actions": true
3498 }
3499 }
3500 }
3501 }
3502 "#
3503 .unindent(),
3504 Some(
3505 &r#"
3506 {
3507 "profiles": {
3508 "work": {
3509 "agent": {
3510 "tool_permissions": {
3511 "default": "allow"
3512 }
3513 }
3514 }
3515 }
3516 }
3517 "#
3518 .unindent(),
3519 ),
3520 );
3521
3522 // Root-level profile with default_mode
3523 assert_migrate_settings_with_migrations(
3524 &[MigrationType::Json(
3525 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3526 )],
3527 &r#"
3528 {
3529 "profiles": {
3530 "work": {
3531 "agent": {
3532 "tool_permissions": {
3533 "default_mode": "allow"
3534 }
3535 }
3536 }
3537 }
3538 }
3539 "#
3540 .unindent(),
3541 Some(
3542 &r#"
3543 {
3544 "profiles": {
3545 "work": {
3546 "agent": {
3547 "tool_permissions": {
3548 "default": "allow"
3549 }
3550 }
3551 }
3552 }
3553 }
3554 "#
3555 .unindent(),
3556 ),
3557 );
3558
3559 // Root-level profile + root-level agent both migrated
3560 assert_migrate_settings_with_migrations(
3561 &[MigrationType::Json(
3562 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3563 )],
3564 &r#"
3565 {
3566 "agent": {
3567 "always_allow_tool_actions": true
3568 },
3569 "profiles": {
3570 "strict": {
3571 "agent": {
3572 "tool_permissions": {
3573 "default_mode": "deny"
3574 }
3575 }
3576 }
3577 }
3578 }
3579 "#
3580 .unindent(),
3581 Some(
3582 &r#"
3583 {
3584 "agent": {
3585 "tool_permissions": {
3586 "default": "allow"
3587 }
3588 },
3589 "profiles": {
3590 "strict": {
3591 "agent": {
3592 "tool_permissions": {
3593 "default": "deny"
3594 }
3595 }
3596 }
3597 }
3598 }
3599 "#
3600 .unindent(),
3601 ),
3602 );
3603
3604 // Non-boolean always_allow_tool_actions (string "true") is left in place
3605 // so the schema validator can report it, rather than silently dropping user data.
3606 assert_migrate_settings_with_migrations(
3607 &[MigrationType::Json(
3608 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3609 )],
3610 &r#"
3611 {
3612 "agent": {
3613 "always_allow_tool_actions": "true"
3614 }
3615 }
3616 "#
3617 .unindent(),
3618 None,
3619 );
3620
3621 // null always_allow_tool_actions is removed (treated as false)
3622 assert_migrate_settings_with_migrations(
3623 &[MigrationType::Json(
3624 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3625 )],
3626 &r#"
3627 {
3628 "agent": {
3629 "always_allow_tool_actions": null
3630 }
3631 }
3632 "#
3633 .unindent(),
3634 Some(&"{\n \"agent\": {\n \n }\n}\n"),
3635 );
3636
3637 // Project-local settings (.zed/settings.json) with always_allow_tool_actions
3638 // These files have no platform/channel overrides or root-level profiles.
3639 assert_migrate_settings_with_migrations(
3640 &[MigrationType::Json(
3641 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3642 )],
3643 &r#"
3644 {
3645 "agent": {
3646 "always_allow_tool_actions": true,
3647 "tool_permissions": {
3648 "tools": {
3649 "terminal": {
3650 "default_mode": "confirm",
3651 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3652 }
3653 }
3654 }
3655 }
3656 }
3657 "#
3658 .unindent(),
3659 Some(
3660 &r#"
3661 {
3662 "agent": {
3663 "tool_permissions": {
3664 "default": "allow",
3665 "tools": {
3666 "terminal": {
3667 "default": "confirm",
3668 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3669 }
3670 }
3671 }
3672 }
3673 }
3674 "#
3675 .unindent(),
3676 ),
3677 );
3678
3679 // Project-local settings with only default_mode (no always_allow_tool_actions)
3680 assert_migrate_settings_with_migrations(
3681 &[MigrationType::Json(
3682 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3683 )],
3684 &r#"
3685 {
3686 "agent": {
3687 "tool_permissions": {
3688 "default_mode": "deny"
3689 }
3690 }
3691 }
3692 "#
3693 .unindent(),
3694 Some(
3695 &r#"
3696 {
3697 "agent": {
3698 "tool_permissions": {
3699 "default": "deny"
3700 }
3701 }
3702 }
3703 "#
3704 .unindent(),
3705 ),
3706 );
3707
3708 // Project-local settings with no agent section at all - no change
3709 assert_migrate_settings_with_migrations(
3710 &[MigrationType::Json(
3711 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3712 )],
3713 &r#"
3714 {
3715 "tab_size": 4,
3716 "format_on_save": "on"
3717 }
3718 "#
3719 .unindent(),
3720 None,
3721 );
3722
3723 // Existing agent_servers are left untouched
3724 assert_migrate_settings_with_migrations(
3725 &[MigrationType::Json(
3726 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3727 )],
3728 &r#"
3729 {
3730 "agent": {
3731 "always_allow_tool_actions": true
3732 },
3733 "agent_servers": {
3734 "claude": {
3735 "default_mode": "plan"
3736 },
3737 "codex": {
3738 "default_mode": "read-only"
3739 }
3740 }
3741 }
3742 "#
3743 .unindent(),
3744 Some(
3745 &r#"
3746 {
3747 "agent": {
3748 "tool_permissions": {
3749 "default": "allow"
3750 }
3751 },
3752 "agent_servers": {
3753 "claude": {
3754 "default_mode": "plan"
3755 },
3756 "codex": {
3757 "default_mode": "read-only"
3758 }
3759 }
3760 }
3761 "#
3762 .unindent(),
3763 ),
3764 );
3765
3766 // Existing agent_servers are left untouched even with partial entries
3767 assert_migrate_settings_with_migrations(
3768 &[MigrationType::Json(
3769 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3770 )],
3771 &r#"
3772 {
3773 "agent": {
3774 "always_allow_tool_actions": true
3775 },
3776 "agent_servers": {
3777 "claude": {
3778 "default_mode": "plan"
3779 }
3780 }
3781 }
3782 "#
3783 .unindent(),
3784 Some(
3785 &r#"
3786 {
3787 "agent": {
3788 "tool_permissions": {
3789 "default": "allow"
3790 }
3791 },
3792 "agent_servers": {
3793 "claude": {
3794 "default_mode": "plan"
3795 }
3796 }
3797 }
3798 "#
3799 .unindent(),
3800 ),
3801 );
3802
3803 // always_allow_tool_actions: false leaves agent_servers untouched
3804 assert_migrate_settings_with_migrations(
3805 &[MigrationType::Json(
3806 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3807 )],
3808 &r#"
3809 {
3810 "agent": {
3811 "always_allow_tool_actions": false
3812 },
3813 "agent_servers": {
3814 "claude": {}
3815 }
3816 }
3817 "#
3818 .unindent(),
3819 Some(
3820 "{\n \"agent\": {\n \n },\n \"agent_servers\": {\n \"claude\": {}\n }\n}\n",
3821 ),
3822 );
3823 }
3824
3825 #[test]
3826 fn test_migrate_builtin_agent_servers_to_registry_simple() {
3827 assert_migrate_settings_with_migrations(
3828 &[MigrationType::Json(
3829 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
3830 )],
3831 r#"{
3832 "agent_servers": {
3833 "gemini": {
3834 "default_model": "gemini-2.0-flash"
3835 },
3836 "claude": {
3837 "default_mode": "plan"
3838 },
3839 "codex": {
3840 "default_model": "o4-mini"
3841 }
3842 }
3843}"#,
3844 Some(
3845 r#"{
3846 "agent_servers": {
3847 "codex-acp": {
3848 "type": "registry",
3849 "default_model": "o4-mini"
3850 },
3851 "claude-acp": {
3852 "type": "registry",
3853 "default_mode": "plan"
3854 },
3855 "gemini": {
3856 "type": "registry",
3857 "default_model": "gemini-2.0-flash"
3858 }
3859 }
3860}"#,
3861 ),
3862 );
3863 }
3864
3865 #[test]
3866 fn test_migrate_builtin_agent_servers_empty_entries() {
3867 assert_migrate_settings_with_migrations(
3868 &[MigrationType::Json(
3869 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
3870 )],
3871 r#"{
3872 "agent_servers": {
3873 "gemini": {},
3874 "claude": {},
3875 "codex": {}
3876 }
3877}"#,
3878 Some(
3879 r#"{
3880 "agent_servers": {
3881 "codex-acp": {
3882 "type": "registry"
3883 },
3884 "claude-acp": {
3885 "type": "registry"
3886 },
3887 "gemini": {
3888 "type": "registry"
3889 }
3890 }
3891}"#,
3892 ),
3893 );
3894 }
3895
3896 #[test]
3897 fn test_migrate_builtin_agent_servers_with_command() {
3898 assert_migrate_settings_with_migrations(
3899 &[MigrationType::Json(
3900 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
3901 )],
3902 r#"{
3903 "agent_servers": {
3904 "claude": {
3905 "command": "/usr/local/bin/claude",
3906 "args": ["--verbose"],
3907 "env": {"CLAUDE_KEY": "abc123"},
3908 "default_mode": "plan",
3909 "default_model": "claude-sonnet-4"
3910 }
3911 }
3912}"#,
3913 Some(
3914 r#"{
3915 "agent_servers": {
3916 "claude-acp-custom": {
3917 "type": "custom",
3918 "command": "/usr/local/bin/claude",
3919 "args": [
3920 "--verbose"
3921 ],
3922 "env": {
3923 "CLAUDE_KEY": "abc123"
3924 },
3925 "default_mode": "plan",
3926 "default_model": "claude-sonnet-4"
3927 }
3928 }
3929}"#,
3930 ),
3931 );
3932 }
3933
3934 #[test]
3935 fn test_migrate_builtin_agent_servers_gemini_with_command() {
3936 assert_migrate_settings_with_migrations(
3937 &[MigrationType::Json(
3938 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
3939 )],
3940 r#"{
3941 "agent_servers": {
3942 "gemini": {
3943 "command": "/opt/gemini/bin/gemini",
3944 "default_model": "gemini-2.0-flash"
3945 }
3946 }
3947}"#,
3948 Some(
3949 r#"{
3950 "agent_servers": {
3951 "gemini-custom": {
3952 "type": "custom",
3953 "command": "/opt/gemini/bin/gemini",
3954 "default_model": "gemini-2.0-flash"
3955 }
3956 }
3957}"#,
3958 ),
3959 );
3960 }
3961
3962 #[test]
3963 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_false() {
3964 assert_migrate_settings_with_migrations(
3965 &[MigrationType::Json(
3966 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
3967 )],
3968 r#"{
3969 "agent_servers": {
3970 "gemini": {
3971 "ignore_system_version": false,
3972 "default_model": "gemini-2.0-flash"
3973 }
3974 }
3975}"#,
3976 Some(
3977 r#"{
3978 "agent_servers": {
3979 "gemini-custom": {
3980 "type": "custom",
3981 "command": "gemini",
3982 "default_model": "gemini-2.0-flash"
3983 }
3984 }
3985}"#,
3986 ),
3987 );
3988 }
3989
3990 #[test]
3991 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_true() {
3992 assert_migrate_settings_with_migrations(
3993 &[MigrationType::Json(
3994 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
3995 )],
3996 r#"{
3997 "agent_servers": {
3998 "gemini": {
3999 "ignore_system_version": true,
4000 "default_model": "gemini-2.0-flash"
4001 }
4002 }
4003}"#,
4004 Some(
4005 r#"{
4006 "agent_servers": {
4007 "gemini": {
4008 "type": "registry",
4009 "default_model": "gemini-2.0-flash"
4010 }
4011 }
4012}"#,
4013 ),
4014 );
4015 }
4016
4017 #[test]
4018 fn test_migrate_builtin_agent_servers_already_typed_unchanged() {
4019 assert_migrate_settings_with_migrations(
4020 &[MigrationType::Json(
4021 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4022 )],
4023 r#"{
4024 "agent_servers": {
4025 "gemini": {
4026 "type": "registry",
4027 "default_model": "gemini-2.0-flash"
4028 },
4029 "claude-acp": {
4030 "type": "registry",
4031 "default_mode": "plan"
4032 }
4033 }
4034}"#,
4035 None,
4036 );
4037 }
4038
4039 #[test]
4040 fn test_migrate_builtin_agent_servers_preserves_custom_entries() {
4041 assert_migrate_settings_with_migrations(
4042 &[MigrationType::Json(
4043 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4044 )],
4045 r#"{
4046 "agent_servers": {
4047 "claude": {
4048 "default_mode": "plan"
4049 },
4050 "my-custom-agent": {
4051 "type": "custom",
4052 "command": "/path/to/agent"
4053 }
4054 }
4055}"#,
4056 Some(
4057 r#"{
4058 "agent_servers": {
4059 "claude-acp": {
4060 "type": "registry",
4061 "default_mode": "plan"
4062 },
4063 "my-custom-agent": {
4064 "type": "custom",
4065 "command": "/path/to/agent"
4066 }
4067 }
4068}"#,
4069 ),
4070 );
4071 }
4072
4073 #[test]
4074 fn test_migrate_builtin_agent_servers_target_already_exists() {
4075 assert_migrate_settings_with_migrations(
4076 &[MigrationType::Json(
4077 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4078 )],
4079 r#"{
4080 "agent_servers": {
4081 "claude": {
4082 "default_mode": "plan"
4083 },
4084 "claude-acp": {
4085 "type": "registry",
4086 "default_model": "claude-sonnet-4"
4087 }
4088 }
4089}"#,
4090 Some(
4091 r#"{
4092 "agent_servers": {
4093 "claude-acp": {
4094 "type": "registry",
4095 "default_model": "claude-sonnet-4"
4096 }
4097 }
4098}"#,
4099 ),
4100 );
4101 }
4102
4103 #[test]
4104 fn test_migrate_builtin_agent_servers_no_agent_servers_key() {
4105 assert_migrate_settings_with_migrations(
4106 &[MigrationType::Json(
4107 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4108 )],
4109 r#"{
4110 "agent": {
4111 "enabled": true
4112 }
4113}"#,
4114 None,
4115 );
4116 }
4117
4118 #[test]
4119 fn test_migrate_builtin_agent_servers_all_fields() {
4120 assert_migrate_settings_with_migrations(
4121 &[MigrationType::Json(
4122 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4123 )],
4124 r#"{
4125 "agent_servers": {
4126 "codex": {
4127 "env": {"OPENAI_API_KEY": "sk-123"},
4128 "default_mode": "read-only",
4129 "default_model": "o4-mini",
4130 "favorite_models": ["o4-mini", "codex-mini-latest"],
4131 "default_config_options": {"approval_mode": "auto-edit"},
4132 "favorite_config_option_values": {"approval_mode": ["auto-edit", "suggest"]}
4133 }
4134 }
4135}"#,
4136 Some(
4137 r#"{
4138 "agent_servers": {
4139 "codex-acp": {
4140 "type": "registry",
4141 "env": {
4142 "OPENAI_API_KEY": "sk-123"
4143 },
4144 "default_mode": "read-only",
4145 "default_model": "o4-mini",
4146 "favorite_models": [
4147 "o4-mini",
4148 "codex-mini-latest"
4149 ],
4150 "default_config_options": {
4151 "approval_mode": "auto-edit"
4152 },
4153 "favorite_config_option_values": {
4154 "approval_mode": [
4155 "auto-edit",
4156 "suggest"
4157 ]
4158 }
4159 }
4160 }
4161}"#,
4162 ),
4163 );
4164 }
4165
4166 #[test]
4167 fn test_migrate_builtin_agent_servers_codex_with_command() {
4168 assert_migrate_settings_with_migrations(
4169 &[MigrationType::Json(
4170 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4171 )],
4172 r#"{
4173 "agent_servers": {
4174 "codex": {
4175 "command": "/usr/local/bin/codex",
4176 "args": ["--full-auto"],
4177 "default_model": "o4-mini"
4178 }
4179 }
4180}"#,
4181 Some(
4182 r#"{
4183 "agent_servers": {
4184 "codex-acp-custom": {
4185 "type": "custom",
4186 "command": "/usr/local/bin/codex",
4187 "args": [
4188 "--full-auto"
4189 ],
4190 "default_model": "o4-mini"
4191 }
4192 }
4193}"#,
4194 ),
4195 );
4196 }
4197
4198 #[test]
4199 fn test_migrate_builtin_agent_servers_mixed_migrated_and_not() {
4200 assert_migrate_settings_with_migrations(
4201 &[MigrationType::Json(
4202 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4203 )],
4204 r#"{
4205 "agent_servers": {
4206 "gemini": {
4207 "type": "registry",
4208 "default_model": "gemini-2.0-flash"
4209 },
4210 "claude": {
4211 "default_mode": "plan"
4212 },
4213 "codex": {}
4214 }
4215}"#,
4216 Some(
4217 r#"{
4218 "agent_servers": {
4219 "codex-acp": {
4220 "type": "registry"
4221 },
4222 "claude-acp": {
4223 "type": "registry",
4224 "default_mode": "plan"
4225 },
4226 "gemini": {
4227 "type": "registry",
4228 "default_model": "gemini-2.0-flash"
4229 }
4230 }
4231}"#,
4232 ),
4233 );
4234 }
4235}