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