migrator.rs

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