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