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