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