Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:36:45.725Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

migrator.rs

5426 lines · 153.6 KB · rust
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(&current_text);
79    for migration in migrations.iter() {
80        let migrated_text = match migration {
81            MigrationType::TreeSitter(patterns, query) => migrate(&current_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(&current_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
Served at tenant.openagents/omega Member data and write actions are omitted.