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