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