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