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