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