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