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