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