migrator.rs

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