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