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