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_03_30::make_play_sound_when_agent_done_an_enum),
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_make_play_sound_when_agent_done_an_enum() {
2406 assert_migrate_with_migrations(
2407 &[MigrationType::Json(
2408 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2409 )],
2410 &r#"{ }"#.unindent(),
2411 None,
2412 );
2413
2414 assert_migrate_with_migrations(
2415 &[MigrationType::Json(
2416 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2417 )],
2418 &r#"{
2419 "agent": {
2420 "play_sound_when_agent_done": true
2421 }
2422 }"#
2423 .unindent(),
2424 Some(
2425 &r#"{
2426 "agent": {
2427 "play_sound_when_agent_done": "always"
2428 }
2429 }"#
2430 .unindent(),
2431 ),
2432 );
2433
2434 assert_migrate_with_migrations(
2435 &[MigrationType::Json(
2436 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2437 )],
2438 &r#"{
2439 "agent": {
2440 "play_sound_when_agent_done": false
2441 }
2442 }"#
2443 .unindent(),
2444 Some(
2445 &r#"{
2446 "agent": {
2447 "play_sound_when_agent_done": "never"
2448 }
2449 }"#
2450 .unindent(),
2451 ),
2452 );
2453
2454 assert_migrate_with_migrations(
2455 &[MigrationType::Json(
2456 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2457 )],
2458 &r#"{
2459 "agent": {
2460 "play_sound_when_agent_done": "when_hidden"
2461 }
2462 }"#
2463 .unindent(),
2464 None,
2465 );
2466
2467 // Platform key: settings nested inside "macos" should be migrated
2468 assert_migrate_with_migrations(
2469 &[MigrationType::Json(
2470 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2471 )],
2472 &r#"
2473 {
2474 "macos": {
2475 "agent": {
2476 "play_sound_when_agent_done": true
2477 }
2478 }
2479 }
2480 "#
2481 .unindent(),
2482 Some(
2483 &r#"
2484 {
2485 "macos": {
2486 "agent": {
2487 "play_sound_when_agent_done": "always"
2488 }
2489 }
2490 }
2491 "#
2492 .unindent(),
2493 ),
2494 );
2495
2496 // Profile: settings nested inside profiles should be migrated
2497 assert_migrate_with_migrations(
2498 &[MigrationType::Json(
2499 migrations::m_2026_03_30::make_play_sound_when_agent_done_an_enum,
2500 )],
2501 &r#"
2502 {
2503 "profiles": {
2504 "work": {
2505 "agent": {
2506 "play_sound_when_agent_done": false
2507 }
2508 }
2509 }
2510 }
2511 "#
2512 .unindent(),
2513 Some(
2514 &r#"
2515 {
2516 "profiles": {
2517 "work": {
2518 "agent": {
2519 "play_sound_when_agent_done": "never"
2520 }
2521 }
2522 }
2523 }
2524 "#
2525 .unindent(),
2526 ),
2527 );
2528 }
2529
2530 #[test]
2531 fn test_remove_context_server_source() {
2532 assert_migrate_settings(
2533 &r#"
2534 {
2535 "context_servers": {
2536 "extension_server": {
2537 "source": "extension",
2538 "settings": {
2539 "foo": "bar"
2540 }
2541 },
2542 "custom_server": {
2543 "source": "custom",
2544 "command": "foo",
2545 "args": ["bar"],
2546 "env": {
2547 "FOO": "BAR"
2548 }
2549 },
2550 }
2551 }
2552 "#
2553 .unindent(),
2554 Some(
2555 &r#"
2556 {
2557 "context_servers": {
2558 "extension_server": {
2559 "settings": {
2560 "foo": "bar"
2561 }
2562 },
2563 "custom_server": {
2564 "command": "foo",
2565 "args": ["bar"],
2566 "env": {
2567 "FOO": "BAR"
2568 }
2569 },
2570 }
2571 }
2572 "#
2573 .unindent(),
2574 ),
2575 );
2576
2577 // Platform key: settings nested inside "linux" should be migrated
2578 assert_migrate_with_migrations(
2579 &[MigrationType::Json(
2580 migrations::m_2025_11_25::remove_context_server_source,
2581 )],
2582 &r#"
2583 {
2584 "linux": {
2585 "context_servers": {
2586 "my_server": {
2587 "source": "extension",
2588 "settings": {
2589 "key": "value"
2590 }
2591 }
2592 }
2593 }
2594 }
2595 "#
2596 .unindent(),
2597 Some(
2598 &r#"
2599 {
2600 "linux": {
2601 "context_servers": {
2602 "my_server": {
2603 "settings": {
2604 "key": "value"
2605 }
2606 }
2607 }
2608 }
2609 }
2610 "#
2611 .unindent(),
2612 ),
2613 );
2614
2615 // Profile: settings nested inside profiles should be migrated
2616 assert_migrate_with_migrations(
2617 &[MigrationType::Json(
2618 migrations::m_2025_11_25::remove_context_server_source,
2619 )],
2620 &r#"
2621 {
2622 "profiles": {
2623 "work": {
2624 "context_servers": {
2625 "my_server": {
2626 "source": "custom",
2627 "command": "foo",
2628 "args": ["bar"]
2629 }
2630 }
2631 }
2632 }
2633 }
2634 "#
2635 .unindent(),
2636 Some(
2637 &r#"
2638 {
2639 "profiles": {
2640 "work": {
2641 "context_servers": {
2642 "my_server": {
2643 "command": "foo",
2644 "args": ["bar"]
2645 }
2646 }
2647 }
2648 }
2649 }
2650 "#
2651 .unindent(),
2652 ),
2653 );
2654 }
2655
2656 #[test]
2657 fn test_project_panel_open_file_on_paste_migration() {
2658 assert_migrate_settings(
2659 &r#"
2660 {
2661 "project_panel": {
2662 "open_file_on_paste": true
2663 }
2664 }
2665 "#
2666 .unindent(),
2667 Some(
2668 &r#"
2669 {
2670 "project_panel": {
2671 "auto_open": { "on_paste": true }
2672 }
2673 }
2674 "#
2675 .unindent(),
2676 ),
2677 );
2678
2679 assert_migrate_settings(
2680 &r#"
2681 {
2682 "project_panel": {
2683 "open_file_on_paste": false
2684 }
2685 }
2686 "#
2687 .unindent(),
2688 Some(
2689 &r#"
2690 {
2691 "project_panel": {
2692 "auto_open": { "on_paste": false }
2693 }
2694 }
2695 "#
2696 .unindent(),
2697 ),
2698 );
2699 }
2700
2701 #[test]
2702 fn test_enable_preview_from_code_navigation_migration() {
2703 assert_migrate_settings(
2704 &r#"
2705 {
2706 "other_setting_1": 1,
2707 "preview_tabs": {
2708 "other_setting_2": 2,
2709 "enable_preview_from_code_navigation": false
2710 }
2711 }
2712 "#
2713 .unindent(),
2714 Some(
2715 &r#"
2716 {
2717 "other_setting_1": 1,
2718 "preview_tabs": {
2719 "other_setting_2": 2,
2720 "enable_keep_preview_on_code_navigation": false
2721 }
2722 }
2723 "#
2724 .unindent(),
2725 ),
2726 );
2727
2728 assert_migrate_settings(
2729 &r#"
2730 {
2731 "other_setting_1": 1,
2732 "preview_tabs": {
2733 "other_setting_2": 2,
2734 "enable_preview_from_code_navigation": true
2735 }
2736 }
2737 "#
2738 .unindent(),
2739 Some(
2740 &r#"
2741 {
2742 "other_setting_1": 1,
2743 "preview_tabs": {
2744 "other_setting_2": 2,
2745 "enable_keep_preview_on_code_navigation": true
2746 }
2747 }
2748 "#
2749 .unindent(),
2750 ),
2751 );
2752 }
2753
2754 #[test]
2755 fn test_make_auto_indent_an_enum() {
2756 // Empty settings should not change
2757 assert_migrate_with_migrations(
2758 &[MigrationType::Json(
2759 migrations::m_2025_01_27::make_auto_indent_an_enum,
2760 )],
2761 &r#"{ }"#.unindent(),
2762 None,
2763 );
2764
2765 // true should become "syntax_aware"
2766 assert_migrate_with_migrations(
2767 &[MigrationType::Json(
2768 migrations::m_2025_01_27::make_auto_indent_an_enum,
2769 )],
2770 &r#"{
2771 "auto_indent": true
2772 }"#
2773 .unindent(),
2774 Some(
2775 &r#"{
2776 "auto_indent": "syntax_aware"
2777 }"#
2778 .unindent(),
2779 ),
2780 );
2781
2782 // false should become "none"
2783 assert_migrate_with_migrations(
2784 &[MigrationType::Json(
2785 migrations::m_2025_01_27::make_auto_indent_an_enum,
2786 )],
2787 &r#"{
2788 "auto_indent": false
2789 }"#
2790 .unindent(),
2791 Some(
2792 &r#"{
2793 "auto_indent": "none"
2794 }"#
2795 .unindent(),
2796 ),
2797 );
2798
2799 // Already valid enum values should not change
2800 assert_migrate_with_migrations(
2801 &[MigrationType::Json(
2802 migrations::m_2025_01_27::make_auto_indent_an_enum,
2803 )],
2804 &r#"{
2805 "auto_indent": "preserve_indent"
2806 }"#
2807 .unindent(),
2808 None,
2809 );
2810
2811 // Should also work inside languages
2812 assert_migrate_with_migrations(
2813 &[MigrationType::Json(
2814 migrations::m_2025_01_27::make_auto_indent_an_enum,
2815 )],
2816 &r#"{
2817 "auto_indent": true,
2818 "languages": {
2819 "Python": {
2820 "auto_indent": false
2821 }
2822 }
2823 }"#
2824 .unindent(),
2825 Some(
2826 &r#"{
2827 "auto_indent": "syntax_aware",
2828 "languages": {
2829 "Python": {
2830 "auto_indent": "none"
2831 }
2832 }
2833 }"#
2834 .unindent(),
2835 ),
2836 );
2837 }
2838
2839 #[test]
2840 fn test_move_edit_prediction_provider_to_edit_predictions() {
2841 assert_migrate_with_migrations(
2842 &[MigrationType::Json(
2843 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2844 )],
2845 &r#"{ }"#.unindent(),
2846 None,
2847 );
2848
2849 assert_migrate_with_migrations(
2850 &[MigrationType::Json(
2851 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2852 )],
2853 &r#"
2854 {
2855 "features": {
2856 "edit_prediction_provider": "copilot"
2857 }
2858 }
2859 "#
2860 .unindent(),
2861 Some(
2862 &r#"
2863 {
2864 "edit_predictions": {
2865 "provider": "copilot"
2866 }
2867 }
2868 "#
2869 .unindent(),
2870 ),
2871 );
2872
2873 assert_migrate_with_migrations(
2874 &[MigrationType::Json(
2875 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2876 )],
2877 &r#"
2878 {
2879 "features": {
2880 "edit_prediction_provider": "zed"
2881 },
2882 "edit_predictions": {
2883 "mode": "eager"
2884 }
2885 }
2886 "#
2887 .unindent(),
2888 Some(
2889 &r#"
2890 {
2891 "edit_predictions": {
2892 "provider": "zed",
2893 "mode": "eager"
2894 }
2895 }
2896 "#
2897 .unindent(),
2898 ),
2899 );
2900
2901 assert_migrate_with_migrations(
2902 &[MigrationType::Json(
2903 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2904 )],
2905 &r#"
2906 {
2907 "features": {
2908 "edit_prediction_provider": "supermaven"
2909 },
2910 "edit_predictions": {
2911 "provider": "copilot"
2912 }
2913 }
2914 "#
2915 .unindent(),
2916 Some(
2917 &r#"
2918 {
2919 "edit_predictions": {
2920 "provider": "copilot"
2921 }
2922 }
2923 "#
2924 .unindent(),
2925 ),
2926 );
2927
2928 assert_migrate_with_migrations(
2929 &[MigrationType::Json(
2930 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2931 )],
2932 &r#"
2933 {
2934 "edit_predictions": {
2935 "provider": "zed"
2936 }
2937 }
2938 "#
2939 .unindent(),
2940 None,
2941 );
2942
2943 // Non-object edit_predictions (e.g. true) should gracefully skip
2944 // instead of bail!-ing and aborting the entire migration chain.
2945 assert_migrate_with_migrations(
2946 &[MigrationType::Json(
2947 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2948 )],
2949 &r#"
2950 {
2951 "features": {
2952 "edit_prediction_provider": "copilot"
2953 },
2954 "edit_predictions": true
2955 }
2956 "#
2957 .unindent(),
2958 Some(
2959 &r#"
2960 {
2961 "edit_predictions": true
2962 }
2963 "#
2964 .unindent(),
2965 ),
2966 );
2967
2968 // Platform key: settings nested inside "macos" should be migrated
2969 assert_migrate_with_migrations(
2970 &[MigrationType::Json(
2971 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
2972 )],
2973 &r#"
2974 {
2975 "macos": {
2976 "features": {
2977 "edit_prediction_provider": "copilot"
2978 }
2979 }
2980 }
2981 "#
2982 .unindent(),
2983 Some(
2984 &r#"
2985 {
2986 "macos": {
2987 "edit_predictions": {
2988 "provider": "copilot"
2989 }
2990 }
2991 }
2992 "#
2993 .unindent(),
2994 ),
2995 );
2996
2997 // Profile: settings nested inside profiles should be migrated
2998 assert_migrate_with_migrations(
2999 &[MigrationType::Json(
3000 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
3001 )],
3002 &r#"
3003 {
3004 "profiles": {
3005 "work": {
3006 "features": {
3007 "edit_prediction_provider": "copilot"
3008 }
3009 }
3010 }
3011 }
3012 "#
3013 .unindent(),
3014 Some(
3015 &r#"
3016 {
3017 "profiles": {
3018 "work": {
3019 "edit_predictions": {
3020 "provider": "copilot"
3021 }
3022 }
3023 }
3024 }
3025 "#
3026 .unindent(),
3027 ),
3028 );
3029
3030 // Combined: root + platform + profile should all be migrated simultaneously
3031 assert_migrate_with_migrations(
3032 &[MigrationType::Json(
3033 migrations::m_2026_02_02::move_edit_prediction_provider_to_edit_predictions,
3034 )],
3035 &r#"
3036 {
3037 "features": {
3038 "edit_prediction_provider": "copilot"
3039 },
3040 "macos": {
3041 "features": {
3042 "edit_prediction_provider": "zed"
3043 }
3044 },
3045 "profiles": {
3046 "work": {
3047 "features": {
3048 "edit_prediction_provider": "supermaven"
3049 }
3050 }
3051 }
3052 }
3053 "#
3054 .unindent(),
3055 Some(
3056 &r#"
3057 {
3058 "edit_predictions": {
3059 "provider": "copilot"
3060 },
3061 "macos": {
3062 "edit_predictions": {
3063 "provider": "zed"
3064 }
3065 },
3066 "profiles": {
3067 "work": {
3068 "edit_predictions": {
3069 "provider": "supermaven"
3070 }
3071 }
3072 }
3073 }
3074 "#
3075 .unindent(),
3076 ),
3077 );
3078 }
3079
3080 #[test]
3081 fn test_migrate_experimental_sweep_mercury() {
3082 assert_migrate_with_migrations(
3083 &[MigrationType::Json(
3084 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3085 )],
3086 &r#"{ }"#.unindent(),
3087 None,
3088 );
3089
3090 assert_migrate_with_migrations(
3091 &[MigrationType::Json(
3092 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3093 )],
3094 &r#"
3095 {
3096 "edit_predictions": {
3097 "provider": {
3098 "experimental": "sweep"
3099 }
3100 }
3101 }
3102 "#
3103 .unindent(),
3104 Some(
3105 &r#"
3106 {
3107 "edit_predictions": {
3108 "provider": "sweep"
3109 }
3110 }
3111 "#
3112 .unindent(),
3113 ),
3114 );
3115
3116 assert_migrate_with_migrations(
3117 &[MigrationType::Json(
3118 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3119 )],
3120 &r#"
3121 {
3122 "edit_predictions": {
3123 "provider": {
3124 "experimental": "mercury"
3125 }
3126 }
3127 }
3128 "#
3129 .unindent(),
3130 Some(
3131 &r#"
3132 {
3133 "edit_predictions": {
3134 "provider": "mercury"
3135 }
3136 }
3137 "#
3138 .unindent(),
3139 ),
3140 );
3141
3142 assert_migrate_with_migrations(
3143 &[MigrationType::Json(
3144 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3145 )],
3146 &r#"
3147 {
3148 "features": {
3149 "edit_prediction_provider": {
3150 "experimental": "sweep"
3151 }
3152 }
3153 }
3154 "#
3155 .unindent(),
3156 Some(
3157 &r#"
3158 {
3159 "features": {
3160 "edit_prediction_provider": "sweep"
3161 }
3162 }
3163 "#
3164 .unindent(),
3165 ),
3166 );
3167
3168 assert_migrate_with_migrations(
3169 &[MigrationType::Json(
3170 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3171 )],
3172 &r#"
3173 {
3174 "edit_predictions": {
3175 "provider": "zed"
3176 }
3177 }
3178 "#
3179 .unindent(),
3180 None,
3181 );
3182
3183 assert_migrate_with_migrations(
3184 &[MigrationType::Json(
3185 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3186 )],
3187 &r#"
3188 {
3189 "edit_predictions": {
3190 "provider": {
3191 "experimental": "zeta2"
3192 }
3193 }
3194 }
3195 "#
3196 .unindent(),
3197 None,
3198 );
3199
3200 // Platform key: settings nested inside "linux" should be migrated
3201 assert_migrate_with_migrations(
3202 &[MigrationType::Json(
3203 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3204 )],
3205 &r#"
3206 {
3207 "linux": {
3208 "edit_predictions": {
3209 "provider": {
3210 "experimental": "sweep"
3211 }
3212 }
3213 }
3214 }
3215 "#
3216 .unindent(),
3217 Some(
3218 &r#"
3219 {
3220 "linux": {
3221 "edit_predictions": {
3222 "provider": "sweep"
3223 }
3224 }
3225 }
3226 "#
3227 .unindent(),
3228 ),
3229 );
3230
3231 // Profile: settings nested inside profiles should be migrated
3232 assert_migrate_with_migrations(
3233 &[MigrationType::Json(
3234 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3235 )],
3236 &r#"
3237 {
3238 "profiles": {
3239 "dev": {
3240 "edit_predictions": {
3241 "provider": {
3242 "experimental": "mercury"
3243 }
3244 }
3245 }
3246 }
3247 }
3248 "#
3249 .unindent(),
3250 Some(
3251 &r#"
3252 {
3253 "profiles": {
3254 "dev": {
3255 "edit_predictions": {
3256 "provider": "mercury"
3257 }
3258 }
3259 }
3260 }
3261 "#
3262 .unindent(),
3263 ),
3264 );
3265
3266 // Combined: root + platform + profile should all be migrated simultaneously
3267 assert_migrate_with_migrations(
3268 &[MigrationType::Json(
3269 migrations::m_2026_02_03::migrate_experimental_sweep_mercury,
3270 )],
3271 &r#"
3272 {
3273 "edit_predictions": {
3274 "provider": {
3275 "experimental": "sweep"
3276 }
3277 },
3278 "linux": {
3279 "edit_predictions": {
3280 "provider": {
3281 "experimental": "mercury"
3282 }
3283 }
3284 },
3285 "profiles": {
3286 "dev": {
3287 "edit_predictions": {
3288 "provider": {
3289 "experimental": "sweep"
3290 }
3291 }
3292 }
3293 }
3294 }
3295 "#
3296 .unindent(),
3297 Some(
3298 &r#"
3299 {
3300 "edit_predictions": {
3301 "provider": "sweep"
3302 },
3303 "linux": {
3304 "edit_predictions": {
3305 "provider": "mercury"
3306 }
3307 },
3308 "profiles": {
3309 "dev": {
3310 "edit_predictions": {
3311 "provider": "sweep"
3312 }
3313 }
3314 }
3315 }
3316 "#
3317 .unindent(),
3318 ),
3319 );
3320 }
3321
3322 #[test]
3323 fn test_migrate_always_allow_tool_actions_to_default() {
3324 // No agent settings - no change
3325 assert_migrate_with_migrations(
3326 &[MigrationType::Json(
3327 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3328 )],
3329 &r#"{ }"#.unindent(),
3330 None,
3331 );
3332
3333 // always_allow_tool_actions: true -> tool_permissions.default: "allow"
3334 assert_migrate_with_migrations(
3335 &[MigrationType::Json(
3336 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3337 )],
3338 &r#"
3339 {
3340 "agent": {
3341 "always_allow_tool_actions": true
3342 }
3343 }
3344 "#
3345 .unindent(),
3346 Some(
3347 &r#"
3348 {
3349 "agent": {
3350 "tool_permissions": {
3351 "default": "allow"
3352 }
3353 }
3354 }
3355 "#
3356 .unindent(),
3357 ),
3358 );
3359
3360 // always_allow_tool_actions: false -> just remove it
3361 assert_migrate_with_migrations(
3362 &[MigrationType::Json(
3363 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3364 )],
3365 &r#"
3366 {
3367 "agent": {
3368 "always_allow_tool_actions": false
3369 }
3370 }
3371 "#
3372 .unindent(),
3373 Some(
3374 // The blank line has spaces because the migration preserves the original indentation
3375 "{\n \"agent\": {\n \n }\n}\n",
3376 ),
3377 );
3378
3379 // Preserve existing tool_permissions.tools when migrating
3380 assert_migrate_with_migrations(
3381 &[MigrationType::Json(
3382 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3383 )],
3384 &r#"
3385 {
3386 "agent": {
3387 "always_allow_tool_actions": true,
3388 "tool_permissions": {
3389 "tools": {
3390 "terminal": {
3391 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3392 }
3393 }
3394 }
3395 }
3396 }
3397 "#
3398 .unindent(),
3399 Some(
3400 &r#"
3401 {
3402 "agent": {
3403 "tool_permissions": {
3404 "default": "allow",
3405 "tools": {
3406 "terminal": {
3407 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3408 }
3409 }
3410 }
3411 }
3412 }
3413 "#
3414 .unindent(),
3415 ),
3416 );
3417
3418 // Don't override existing default (and migrate default_mode to default)
3419 assert_migrate_with_migrations(
3420 &[MigrationType::Json(
3421 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3422 )],
3423 &r#"
3424 {
3425 "agent": {
3426 "always_allow_tool_actions": true,
3427 "tool_permissions": {
3428 "default_mode": "confirm"
3429 }
3430 }
3431 }
3432 "#
3433 .unindent(),
3434 Some(
3435 &r#"
3436 {
3437 "agent": {
3438 "tool_permissions": {
3439 "default": "confirm"
3440 }
3441 }
3442 }
3443 "#
3444 .unindent(),
3445 ),
3446 );
3447
3448 // Migrate existing default_mode to default (no always_allow_tool_actions)
3449 assert_migrate_with_migrations(
3450 &[MigrationType::Json(
3451 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3452 )],
3453 &r#"
3454 {
3455 "agent": {
3456 "tool_permissions": {
3457 "default_mode": "allow"
3458 }
3459 }
3460 }
3461 "#
3462 .unindent(),
3463 Some(
3464 &r#"
3465 {
3466 "agent": {
3467 "tool_permissions": {
3468 "default": "allow"
3469 }
3470 }
3471 }
3472 "#
3473 .unindent(),
3474 ),
3475 );
3476
3477 // No migration needed if already using new format with "default"
3478 assert_migrate_with_migrations(
3479 &[MigrationType::Json(
3480 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3481 )],
3482 &r#"
3483 {
3484 "agent": {
3485 "tool_permissions": {
3486 "default": "allow"
3487 }
3488 }
3489 }
3490 "#
3491 .unindent(),
3492 None,
3493 );
3494
3495 // Migrate default_mode to default in tool-specific rules
3496 assert_migrate_with_migrations(
3497 &[MigrationType::Json(
3498 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3499 )],
3500 &r#"
3501 {
3502 "agent": {
3503 "tool_permissions": {
3504 "default_mode": "confirm",
3505 "tools": {
3506 "terminal": {
3507 "default_mode": "allow"
3508 }
3509 }
3510 }
3511 }
3512 }
3513 "#
3514 .unindent(),
3515 Some(
3516 &r#"
3517 {
3518 "agent": {
3519 "tool_permissions": {
3520 "default": "confirm",
3521 "tools": {
3522 "terminal": {
3523 "default": "allow"
3524 }
3525 }
3526 }
3527 }
3528 }
3529 "#
3530 .unindent(),
3531 ),
3532 );
3533
3534 // When tool_permissions is null, replace it so always_allow is preserved
3535 assert_migrate_with_migrations(
3536 &[MigrationType::Json(
3537 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3538 )],
3539 &r#"
3540 {
3541 "agent": {
3542 "always_allow_tool_actions": true,
3543 "tool_permissions": null
3544 }
3545 }
3546 "#
3547 .unindent(),
3548 Some(
3549 &r#"
3550 {
3551 "agent": {
3552 "tool_permissions": {
3553 "default": "allow"
3554 }
3555 }
3556 }
3557 "#
3558 .unindent(),
3559 ),
3560 );
3561
3562 // Platform-specific agent migration
3563 assert_migrate_with_migrations(
3564 &[MigrationType::Json(
3565 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3566 )],
3567 &r#"
3568 {
3569 "linux": {
3570 "agent": {
3571 "always_allow_tool_actions": true
3572 }
3573 }
3574 }
3575 "#
3576 .unindent(),
3577 Some(
3578 &r#"
3579 {
3580 "linux": {
3581 "agent": {
3582 "tool_permissions": {
3583 "default": "allow"
3584 }
3585 }
3586 }
3587 }
3588 "#
3589 .unindent(),
3590 ),
3591 );
3592
3593 // Channel-specific agent migration
3594 assert_migrate_with_migrations(
3595 &[MigrationType::Json(
3596 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3597 )],
3598 &r#"
3599 {
3600 "agent": {
3601 "always_allow_tool_actions": true
3602 },
3603 "nightly": {
3604 "agent": {
3605 "tool_permissions": {
3606 "default_mode": "confirm"
3607 }
3608 }
3609 }
3610 }
3611 "#
3612 .unindent(),
3613 Some(
3614 &r#"
3615 {
3616 "agent": {
3617 "tool_permissions": {
3618 "default": "allow"
3619 }
3620 },
3621 "nightly": {
3622 "agent": {
3623 "tool_permissions": {
3624 "default": "confirm"
3625 }
3626 }
3627 }
3628 }
3629 "#
3630 .unindent(),
3631 ),
3632 );
3633
3634 // Profile-level migration
3635 assert_migrate_with_migrations(
3636 &[MigrationType::Json(
3637 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3638 )],
3639 &r#"
3640 {
3641 "agent": {
3642 "profiles": {
3643 "custom": {
3644 "always_allow_tool_actions": true,
3645 "tool_permissions": {
3646 "default_mode": "allow"
3647 }
3648 }
3649 }
3650 }
3651 }
3652 "#
3653 .unindent(),
3654 Some(
3655 &r#"
3656 {
3657 "agent": {
3658 "profiles": {
3659 "custom": {
3660 "tool_permissions": {
3661 "default": "allow"
3662 }
3663 }
3664 }
3665 }
3666 }
3667 "#
3668 .unindent(),
3669 ),
3670 );
3671
3672 // Platform-specific agent with profiles
3673 assert_migrate_with_migrations(
3674 &[MigrationType::Json(
3675 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3676 )],
3677 &r#"
3678 {
3679 "macos": {
3680 "agent": {
3681 "always_allow_tool_actions": true,
3682 "profiles": {
3683 "strict": {
3684 "tool_permissions": {
3685 "default_mode": "deny"
3686 }
3687 }
3688 }
3689 }
3690 }
3691 }
3692 "#
3693 .unindent(),
3694 Some(
3695 &r#"
3696 {
3697 "macos": {
3698 "agent": {
3699 "tool_permissions": {
3700 "default": "allow"
3701 },
3702 "profiles": {
3703 "strict": {
3704 "tool_permissions": {
3705 "default": "deny"
3706 }
3707 }
3708 }
3709 }
3710 }
3711 }
3712 "#
3713 .unindent(),
3714 ),
3715 );
3716
3717 // Root-level profile with always_allow_tool_actions
3718 assert_migrate_with_migrations(
3719 &[MigrationType::Json(
3720 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3721 )],
3722 &r#"
3723 {
3724 "profiles": {
3725 "work": {
3726 "agent": {
3727 "always_allow_tool_actions": true
3728 }
3729 }
3730 }
3731 }
3732 "#
3733 .unindent(),
3734 Some(
3735 &r#"
3736 {
3737 "profiles": {
3738 "work": {
3739 "agent": {
3740 "tool_permissions": {
3741 "default": "allow"
3742 }
3743 }
3744 }
3745 }
3746 }
3747 "#
3748 .unindent(),
3749 ),
3750 );
3751
3752 // Root-level profile with default_mode
3753 assert_migrate_with_migrations(
3754 &[MigrationType::Json(
3755 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3756 )],
3757 &r#"
3758 {
3759 "profiles": {
3760 "work": {
3761 "agent": {
3762 "tool_permissions": {
3763 "default_mode": "allow"
3764 }
3765 }
3766 }
3767 }
3768 }
3769 "#
3770 .unindent(),
3771 Some(
3772 &r#"
3773 {
3774 "profiles": {
3775 "work": {
3776 "agent": {
3777 "tool_permissions": {
3778 "default": "allow"
3779 }
3780 }
3781 }
3782 }
3783 }
3784 "#
3785 .unindent(),
3786 ),
3787 );
3788
3789 // Root-level profile + root-level agent both migrated
3790 assert_migrate_with_migrations(
3791 &[MigrationType::Json(
3792 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3793 )],
3794 &r#"
3795 {
3796 "agent": {
3797 "always_allow_tool_actions": true
3798 },
3799 "profiles": {
3800 "strict": {
3801 "agent": {
3802 "tool_permissions": {
3803 "default_mode": "deny"
3804 }
3805 }
3806 }
3807 }
3808 }
3809 "#
3810 .unindent(),
3811 Some(
3812 &r#"
3813 {
3814 "agent": {
3815 "tool_permissions": {
3816 "default": "allow"
3817 }
3818 },
3819 "profiles": {
3820 "strict": {
3821 "agent": {
3822 "tool_permissions": {
3823 "default": "deny"
3824 }
3825 }
3826 }
3827 }
3828 }
3829 "#
3830 .unindent(),
3831 ),
3832 );
3833
3834 // Non-boolean always_allow_tool_actions (string "true") is left in place
3835 // so the schema validator can report it, rather than silently dropping user data.
3836 assert_migrate_with_migrations(
3837 &[MigrationType::Json(
3838 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3839 )],
3840 &r#"
3841 {
3842 "agent": {
3843 "always_allow_tool_actions": "true"
3844 }
3845 }
3846 "#
3847 .unindent(),
3848 None,
3849 );
3850
3851 // null always_allow_tool_actions is removed (treated as false)
3852 assert_migrate_with_migrations(
3853 &[MigrationType::Json(
3854 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3855 )],
3856 &r#"
3857 {
3858 "agent": {
3859 "always_allow_tool_actions": null
3860 }
3861 }
3862 "#
3863 .unindent(),
3864 Some(&"{\n \"agent\": {\n \n }\n}\n"),
3865 );
3866
3867 // Project-local settings (.zed/settings.json) with always_allow_tool_actions
3868 // These files have no platform/channel overrides or root-level profiles.
3869 assert_migrate_with_migrations(
3870 &[MigrationType::Json(
3871 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3872 )],
3873 &r#"
3874 {
3875 "agent": {
3876 "always_allow_tool_actions": true,
3877 "tool_permissions": {
3878 "tools": {
3879 "terminal": {
3880 "default_mode": "confirm",
3881 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3882 }
3883 }
3884 }
3885 }
3886 }
3887 "#
3888 .unindent(),
3889 Some(
3890 &r#"
3891 {
3892 "agent": {
3893 "tool_permissions": {
3894 "default": "allow",
3895 "tools": {
3896 "terminal": {
3897 "default": "confirm",
3898 "always_deny": [{ "pattern": "rm\\s+-rf" }]
3899 }
3900 }
3901 }
3902 }
3903 }
3904 "#
3905 .unindent(),
3906 ),
3907 );
3908
3909 // Project-local settings with only default_mode (no always_allow_tool_actions)
3910 assert_migrate_with_migrations(
3911 &[MigrationType::Json(
3912 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3913 )],
3914 &r#"
3915 {
3916 "agent": {
3917 "tool_permissions": {
3918 "default_mode": "deny"
3919 }
3920 }
3921 }
3922 "#
3923 .unindent(),
3924 Some(
3925 &r#"
3926 {
3927 "agent": {
3928 "tool_permissions": {
3929 "default": "deny"
3930 }
3931 }
3932 }
3933 "#
3934 .unindent(),
3935 ),
3936 );
3937
3938 // Project-local settings with no agent section at all - no change
3939 assert_migrate_with_migrations(
3940 &[MigrationType::Json(
3941 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3942 )],
3943 &r#"
3944 {
3945 "tab_size": 4,
3946 "format_on_save": "on"
3947 }
3948 "#
3949 .unindent(),
3950 None,
3951 );
3952
3953 // Existing agent_servers are left untouched
3954 assert_migrate_with_migrations(
3955 &[MigrationType::Json(
3956 migrations::m_2026_02_04::migrate_tool_permission_defaults,
3957 )],
3958 &r#"
3959 {
3960 "agent": {
3961 "always_allow_tool_actions": true
3962 },
3963 "agent_servers": {
3964 "claude": {
3965 "default_mode": "plan"
3966 },
3967 "codex": {
3968 "default_mode": "read-only"
3969 }
3970 }
3971 }
3972 "#
3973 .unindent(),
3974 Some(
3975 &r#"
3976 {
3977 "agent": {
3978 "tool_permissions": {
3979 "default": "allow"
3980 }
3981 },
3982 "agent_servers": {
3983 "claude": {
3984 "default_mode": "plan"
3985 },
3986 "codex": {
3987 "default_mode": "read-only"
3988 }
3989 }
3990 }
3991 "#
3992 .unindent(),
3993 ),
3994 );
3995
3996 // Existing agent_servers are left untouched even with partial entries
3997 assert_migrate_with_migrations(
3998 &[MigrationType::Json(
3999 migrations::m_2026_02_04::migrate_tool_permission_defaults,
4000 )],
4001 &r#"
4002 {
4003 "agent": {
4004 "always_allow_tool_actions": true
4005 },
4006 "agent_servers": {
4007 "claude": {
4008 "default_mode": "plan"
4009 }
4010 }
4011 }
4012 "#
4013 .unindent(),
4014 Some(
4015 &r#"
4016 {
4017 "agent": {
4018 "tool_permissions": {
4019 "default": "allow"
4020 }
4021 },
4022 "agent_servers": {
4023 "claude": {
4024 "default_mode": "plan"
4025 }
4026 }
4027 }
4028 "#
4029 .unindent(),
4030 ),
4031 );
4032
4033 // always_allow_tool_actions: false leaves agent_servers untouched
4034 assert_migrate_with_migrations(
4035 &[MigrationType::Json(
4036 migrations::m_2026_02_04::migrate_tool_permission_defaults,
4037 )],
4038 &r#"
4039 {
4040 "agent": {
4041 "always_allow_tool_actions": false
4042 },
4043 "agent_servers": {
4044 "claude": {}
4045 }
4046 }
4047 "#
4048 .unindent(),
4049 Some(
4050 "{\n \"agent\": {\n \n },\n \"agent_servers\": {\n \"claude\": {}\n }\n}\n",
4051 ),
4052 );
4053 }
4054
4055 #[test]
4056 fn test_migrate_builtin_agent_servers_to_registry_simple() {
4057 assert_migrate_with_migrations(
4058 &[MigrationType::Json(
4059 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4060 )],
4061 r#"{
4062 "agent_servers": {
4063 "gemini": {
4064 "default_model": "gemini-2.0-flash"
4065 },
4066 "claude": {
4067 "default_mode": "plan"
4068 },
4069 "codex": {
4070 "default_model": "o4-mini"
4071 }
4072 }
4073}"#,
4074 Some(
4075 r#"{
4076 "agent_servers": {
4077 "codex-acp": {
4078 "type": "registry",
4079 "default_model": "o4-mini"
4080 },
4081 "claude-acp": {
4082 "type": "registry",
4083 "default_mode": "plan"
4084 },
4085 "gemini": {
4086 "type": "registry",
4087 "default_model": "gemini-2.0-flash"
4088 }
4089 }
4090}"#,
4091 ),
4092 );
4093 }
4094
4095 #[test]
4096 fn test_migrate_builtin_agent_servers_empty_entries() {
4097 assert_migrate_with_migrations(
4098 &[MigrationType::Json(
4099 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4100 )],
4101 r#"{
4102 "agent_servers": {
4103 "gemini": {},
4104 "claude": {},
4105 "codex": {}
4106 }
4107}"#,
4108 Some(
4109 r#"{
4110 "agent_servers": {
4111 "codex-acp": {
4112 "type": "registry"
4113 },
4114 "claude-acp": {
4115 "type": "registry"
4116 },
4117 "gemini": {
4118 "type": "registry"
4119 }
4120 }
4121}"#,
4122 ),
4123 );
4124 }
4125
4126 #[test]
4127 fn test_migrate_builtin_agent_servers_with_command() {
4128 assert_migrate_with_migrations(
4129 &[MigrationType::Json(
4130 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4131 )],
4132 r#"{
4133 "agent_servers": {
4134 "claude": {
4135 "command": "/usr/local/bin/claude",
4136 "args": ["--verbose"],
4137 "env": {"CLAUDE_KEY": "abc123"},
4138 "default_mode": "plan",
4139 "default_model": "claude-sonnet-4"
4140 }
4141 }
4142}"#,
4143 Some(
4144 r#"{
4145 "agent_servers": {
4146 "claude-acp-custom": {
4147 "type": "custom",
4148 "command": "/usr/local/bin/claude",
4149 "args": [
4150 "--verbose"
4151 ],
4152 "env": {
4153 "CLAUDE_KEY": "abc123"
4154 },
4155 "default_mode": "plan",
4156 "default_model": "claude-sonnet-4"
4157 }
4158 }
4159}"#,
4160 ),
4161 );
4162 }
4163
4164 #[test]
4165 fn test_migrate_builtin_agent_servers_gemini_with_command() {
4166 assert_migrate_with_migrations(
4167 &[MigrationType::Json(
4168 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4169 )],
4170 r#"{
4171 "agent_servers": {
4172 "gemini": {
4173 "command": "/opt/gemini/bin/gemini",
4174 "default_model": "gemini-2.0-flash"
4175 }
4176 }
4177}"#,
4178 Some(
4179 r#"{
4180 "agent_servers": {
4181 "gemini-custom": {
4182 "type": "custom",
4183 "command": "/opt/gemini/bin/gemini",
4184 "default_model": "gemini-2.0-flash"
4185 }
4186 }
4187}"#,
4188 ),
4189 );
4190 }
4191
4192 #[test]
4193 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_false() {
4194 assert_migrate_with_migrations(
4195 &[MigrationType::Json(
4196 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4197 )],
4198 r#"{
4199 "agent_servers": {
4200 "gemini": {
4201 "ignore_system_version": false,
4202 "default_model": "gemini-2.0-flash"
4203 }
4204 }
4205}"#,
4206 Some(
4207 r#"{
4208 "agent_servers": {
4209 "gemini-custom": {
4210 "type": "custom",
4211 "command": "gemini",
4212 "default_model": "gemini-2.0-flash"
4213 }
4214 }
4215}"#,
4216 ),
4217 );
4218 }
4219
4220 #[test]
4221 fn test_migrate_builtin_agent_servers_gemini_ignore_system_version_true() {
4222 assert_migrate_with_migrations(
4223 &[MigrationType::Json(
4224 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4225 )],
4226 r#"{
4227 "agent_servers": {
4228 "gemini": {
4229 "ignore_system_version": true,
4230 "default_model": "gemini-2.0-flash"
4231 }
4232 }
4233}"#,
4234 Some(
4235 r#"{
4236 "agent_servers": {
4237 "gemini": {
4238 "type": "registry",
4239 "default_model": "gemini-2.0-flash"
4240 }
4241 }
4242}"#,
4243 ),
4244 );
4245 }
4246
4247 #[test]
4248 fn test_migrate_builtin_agent_servers_already_typed_unchanged() {
4249 assert_migrate_with_migrations(
4250 &[MigrationType::Json(
4251 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4252 )],
4253 r#"{
4254 "agent_servers": {
4255 "gemini": {
4256 "type": "registry",
4257 "default_model": "gemini-2.0-flash"
4258 },
4259 "claude-acp": {
4260 "type": "registry",
4261 "default_mode": "plan"
4262 }
4263 }
4264}"#,
4265 None,
4266 );
4267 }
4268
4269 #[test]
4270 fn test_migrate_builtin_agent_servers_preserves_custom_entries() {
4271 assert_migrate_with_migrations(
4272 &[MigrationType::Json(
4273 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4274 )],
4275 r#"{
4276 "agent_servers": {
4277 "claude": {
4278 "default_mode": "plan"
4279 },
4280 "my-custom-agent": {
4281 "type": "custom",
4282 "command": "/path/to/agent"
4283 }
4284 }
4285}"#,
4286 Some(
4287 r#"{
4288 "agent_servers": {
4289 "claude-acp": {
4290 "type": "registry",
4291 "default_mode": "plan"
4292 },
4293 "my-custom-agent": {
4294 "type": "custom",
4295 "command": "/path/to/agent"
4296 }
4297 }
4298}"#,
4299 ),
4300 );
4301 }
4302
4303 #[test]
4304 fn test_migrate_builtin_agent_servers_target_already_exists() {
4305 assert_migrate_with_migrations(
4306 &[MigrationType::Json(
4307 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4308 )],
4309 r#"{
4310 "agent_servers": {
4311 "claude": {
4312 "default_mode": "plan"
4313 },
4314 "claude-acp": {
4315 "type": "registry",
4316 "default_model": "claude-sonnet-4"
4317 }
4318 }
4319}"#,
4320 Some(
4321 r#"{
4322 "agent_servers": {
4323 "claude-acp": {
4324 "type": "registry",
4325 "default_model": "claude-sonnet-4"
4326 }
4327 }
4328}"#,
4329 ),
4330 );
4331 }
4332
4333 #[test]
4334 fn test_migrate_builtin_agent_servers_no_agent_servers_key() {
4335 assert_migrate_with_migrations(
4336 &[MigrationType::Json(
4337 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4338 )],
4339 r#"{
4340 "agent": {
4341 "enabled": true
4342 }
4343}"#,
4344 None,
4345 );
4346 }
4347
4348 #[test]
4349 fn test_migrate_builtin_agent_servers_all_fields() {
4350 assert_migrate_with_migrations(
4351 &[MigrationType::Json(
4352 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4353 )],
4354 r#"{
4355 "agent_servers": {
4356 "codex": {
4357 "env": {"OPENAI_API_KEY": "sk-123"},
4358 "default_mode": "read-only",
4359 "default_model": "o4-mini",
4360 "favorite_models": ["o4-mini", "codex-mini-latest"],
4361 "default_config_options": {"approval_mode": "auto-edit"},
4362 "favorite_config_option_values": {"approval_mode": ["auto-edit", "suggest"]}
4363 }
4364 }
4365}"#,
4366 Some(
4367 r#"{
4368 "agent_servers": {
4369 "codex-acp": {
4370 "type": "registry",
4371 "env": {
4372 "OPENAI_API_KEY": "sk-123"
4373 },
4374 "default_mode": "read-only",
4375 "default_model": "o4-mini",
4376 "favorite_models": [
4377 "o4-mini",
4378 "codex-mini-latest"
4379 ],
4380 "default_config_options": {
4381 "approval_mode": "auto-edit"
4382 },
4383 "favorite_config_option_values": {
4384 "approval_mode": [
4385 "auto-edit",
4386 "suggest"
4387 ]
4388 }
4389 }
4390 }
4391}"#,
4392 ),
4393 );
4394 }
4395
4396 #[test]
4397 fn test_migrate_builtin_agent_servers_codex_with_command() {
4398 assert_migrate_with_migrations(
4399 &[MigrationType::Json(
4400 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4401 )],
4402 r#"{
4403 "agent_servers": {
4404 "codex": {
4405 "command": "/usr/local/bin/codex",
4406 "args": ["--full-auto"],
4407 "default_model": "o4-mini"
4408 }
4409 }
4410}"#,
4411 Some(
4412 r#"{
4413 "agent_servers": {
4414 "codex-acp-custom": {
4415 "type": "custom",
4416 "command": "/usr/local/bin/codex",
4417 "args": [
4418 "--full-auto"
4419 ],
4420 "default_model": "o4-mini"
4421 }
4422 }
4423}"#,
4424 ),
4425 );
4426 }
4427
4428 #[test]
4429 fn test_migrate_builtin_agent_servers_mixed_migrated_and_not() {
4430 assert_migrate_with_migrations(
4431 &[MigrationType::Json(
4432 migrations::m_2026_02_25::migrate_builtin_agent_servers_to_registry,
4433 )],
4434 r#"{
4435 "agent_servers": {
4436 "gemini": {
4437 "type": "registry",
4438 "default_model": "gemini-2.0-flash"
4439 },
4440 "claude": {
4441 "default_mode": "plan"
4442 },
4443 "codex": {}
4444 }
4445}"#,
4446 Some(
4447 r#"{
4448 "agent_servers": {
4449 "codex-acp": {
4450 "type": "registry"
4451 },
4452 "claude-acp": {
4453 "type": "registry",
4454 "default_mode": "plan"
4455 },
4456 "gemini": {
4457 "type": "registry",
4458 "default_model": "gemini-2.0-flash"
4459 }
4460 }
4461}"#,
4462 ),
4463 );
4464 }
4465
4466 #[test]
4467 fn test_migrate_edit_prediction_conflict_context() {
4468 assert_migrate_with_migrations(
4469 &[MigrationType::TreeSitter(
4470 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4471 &KEYMAP_QUERY_2026_03_23,
4472 )],
4473 &r#"
4474 [
4475 {
4476 "context": "Editor && edit_prediction_conflict",
4477 "bindings": {
4478 "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding
4479 }
4480 }
4481 ]
4482 "#.unindent(),
4483 Some(
4484 &r#"
4485 [
4486 {
4487 "context": "Editor && (edit_prediction && (showing_completions || in_leading_whitespace))",
4488 "bindings": {
4489 "ctrl-enter": "editor::AcceptEditPrediction" // Example of a modified keybinding
4490 }
4491 }
4492 ]
4493 "#.unindent(),
4494 ),
4495 );
4496
4497 assert_migrate_with_migrations(
4498 &[MigrationType::TreeSitter(
4499 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4500 &KEYMAP_QUERY_2026_03_23,
4501 )],
4502 &r#"
4503 [
4504 {
4505 "context": "Editor && edit_prediction_conflict && !showing_completions",
4506 "bindings": {
4507 // Here we don't require a modifier unless there's a language server completion
4508 "tab": "editor::AcceptEditPrediction"
4509 }
4510 }
4511 ]
4512 "#.unindent(),
4513 Some(
4514 &r#"
4515 [
4516 {
4517 "context": "Editor && (edit_prediction && in_leading_whitespace)",
4518 "bindings": {
4519 // Here we don't require a modifier unless there's a language server completion
4520 "tab": "editor::AcceptEditPrediction"
4521 }
4522 }
4523 ]
4524 "#.unindent(),
4525 ),
4526 );
4527
4528 assert_migrate_with_migrations(
4529 &[MigrationType::TreeSitter(
4530 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4531 &KEYMAP_QUERY_2026_03_23,
4532 )],
4533 &r#"
4534 [
4535 {
4536 "context": "Editor && edit_prediction_conflict && showing_completions",
4537 "bindings": {
4538 "tab": "editor::AcceptEditPrediction"
4539 }
4540 }
4541 ]
4542 "#
4543 .unindent(),
4544 Some(
4545 &r#"
4546 [
4547 {
4548 "context": "Editor && (edit_prediction && showing_completions)",
4549 "bindings": {
4550 "tab": "editor::AcceptEditPrediction"
4551 }
4552 }
4553 ]
4554 "#
4555 .unindent(),
4556 ),
4557 );
4558
4559 assert_migrate_with_migrations(
4560 &[MigrationType::TreeSitter(
4561 migrations::m_2026_03_23::KEYMAP_PATTERNS,
4562 &KEYMAP_QUERY_2026_03_23,
4563 )],
4564 &r#"
4565 [
4566 {
4567 "context": "Editor && edit_prediction",
4568 "bindings": {
4569 "tab": "editor::AcceptEditPrediction",
4570 // Optional: This makes the default `alt-l` binding do nothing.
4571 "alt-l": null
4572 }
4573 },
4574 {
4575 "context": "Editor && edit_prediction_conflict",
4576 "bindings": {
4577 "alt-tab": "editor::AcceptEditPrediction",
4578 // Optional: This makes the default `alt-l` binding do nothing.
4579 "alt-l": null
4580 }
4581 },
4582 ]
4583 "#
4584 .unindent(),
4585 Some(
4586 &r#"
4587 [
4588 {
4589 "context": "Editor && edit_prediction",
4590 "bindings": {
4591 "tab": "editor::AcceptEditPrediction",
4592 // Optional: This makes the default `alt-l` binding do nothing.
4593 "alt-l": null
4594 }
4595 },
4596 {
4597 "context": "Editor && (edit_prediction && (showing_completions || in_leading_whitespace))",
4598 "bindings": {
4599 "alt-tab": "editor::AcceptEditPrediction",
4600 // Optional: This makes the default `alt-l` binding do nothing.
4601 "alt-l": null
4602 }
4603 },
4604 ]
4605 "#
4606 .unindent(),
4607 ),
4608 );
4609 }
4610}