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