migrator.rs

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