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 std::{cmp::Reverse, ops::Range, sync::LazyLock};
  19use streaming_iterator::StreamingIterator;
  20use tree_sitter::{Query, QueryMatch};
  21
  22use patterns::SETTINGS_NESTED_KEY_VALUE_PATTERN;
  23
  24mod migrations;
  25mod patterns;
  26
  27fn migrate(text: &str, patterns: MigrationPatterns, query: &Query) -> Result<Option<String>> {
  28    let mut parser = tree_sitter::Parser::new();
  29    parser.set_language(&tree_sitter_json::LANGUAGE.into())?;
  30    let syntax_tree = parser
  31        .parse(&text, None)
  32        .context("failed to parse settings")?;
  33
  34    let mut cursor = tree_sitter::QueryCursor::new();
  35    let mut matches = cursor.matches(query, syntax_tree.root_node(), text.as_bytes());
  36
  37    let mut edits = vec![];
  38    while let Some(mat) = matches.next() {
  39        if let Some((_, callback)) = patterns.get(mat.pattern_index) {
  40            edits.extend(callback(&text, &mat, query));
  41        }
  42    }
  43
  44    edits.sort_by_key(|(range, _)| (range.start, Reverse(range.end)));
  45    edits.dedup_by(|(range_b, _), (range_a, _)| {
  46        range_a.contains(&range_b.start) || range_a.contains(&range_b.end)
  47    });
  48
  49    if edits.is_empty() {
  50        Ok(None)
  51    } else {
  52        let mut new_text = text.to_string();
  53        for (range, replacement) in edits.iter().rev() {
  54            new_text.replace_range(range.clone(), replacement);
  55        }
  56        if new_text == text {
  57            log::error!(
  58                "Edits computed for configuration migration do not cause a change: {:?}",
  59                edits
  60            );
  61            Ok(None)
  62        } else {
  63            Ok(Some(new_text))
  64        }
  65    }
  66}
  67
  68fn run_migrations(
  69    text: &str,
  70    migrations: &[(MigrationPatterns, &Query)],
  71) -> Result<Option<String>> {
  72    let mut current_text = text.to_string();
  73    let mut result: Option<String> = None;
  74    for (patterns, query) in migrations.iter() {
  75        if let Some(migrated_text) = migrate(&current_text, patterns, query)? {
  76            current_text = migrated_text.clone();
  77            result = Some(migrated_text);
  78        }
  79    }
  80    Ok(result.filter(|new_text| text != new_text))
  81}
  82
  83pub fn migrate_keymap(text: &str) -> Result<Option<String>> {
  84    let migrations: &[(MigrationPatterns, &Query)] = &[
  85        (
  86            migrations::m_2025_01_29::KEYMAP_PATTERNS,
  87            &KEYMAP_QUERY_2025_01_29,
  88        ),
  89        (
  90            migrations::m_2025_01_30::KEYMAP_PATTERNS,
  91            &KEYMAP_QUERY_2025_01_30,
  92        ),
  93        (
  94            migrations::m_2025_03_03::KEYMAP_PATTERNS,
  95            &KEYMAP_QUERY_2025_03_03,
  96        ),
  97        (
  98            migrations::m_2025_03_06::KEYMAP_PATTERNS,
  99            &KEYMAP_QUERY_2025_03_06,
 100        ),
 101        (
 102            migrations::m_2025_04_15::KEYMAP_PATTERNS,
 103            &KEYMAP_QUERY_2025_04_15,
 104        ),
 105    ];
 106    run_migrations(text, migrations)
 107}
 108
 109pub fn migrate_settings(text: &str) -> Result<Option<String>> {
 110    let migrations: &[(MigrationPatterns, &Query)] = &[
 111        (
 112            migrations::m_2025_01_02::SETTINGS_PATTERNS,
 113            &SETTINGS_QUERY_2025_01_02,
 114        ),
 115        (
 116            migrations::m_2025_01_29::SETTINGS_PATTERNS,
 117            &SETTINGS_QUERY_2025_01_29,
 118        ),
 119        (
 120            migrations::m_2025_01_30::SETTINGS_PATTERNS,
 121            &SETTINGS_QUERY_2025_01_30,
 122        ),
 123        (
 124            migrations::m_2025_03_29::SETTINGS_PATTERNS,
 125            &SETTINGS_QUERY_2025_03_29,
 126        ),
 127        (
 128            migrations::m_2025_04_15::SETTINGS_PATTERNS,
 129            &SETTINGS_QUERY_2025_04_15,
 130        ),
 131        (
 132            migrations::m_2025_04_21::SETTINGS_PATTERNS,
 133            &SETTINGS_QUERY_2025_04_21,
 134        ),
 135        (
 136            migrations::m_2025_04_23::SETTINGS_PATTERNS,
 137            &SETTINGS_QUERY_2025_04_23,
 138        ),
 139        (
 140            migrations::m_2025_05_05::SETTINGS_PATTERNS,
 141            &SETTINGS_QUERY_2025_05_05,
 142        ),
 143        (
 144            migrations::m_2025_05_08::SETTINGS_PATTERNS,
 145            &SETTINGS_QUERY_2025_05_08,
 146        ),
 147        (
 148            migrations::m_2025_05_29::SETTINGS_PATTERNS,
 149            &SETTINGS_QUERY_2025_05_29,
 150        ),
 151        (
 152            migrations::m_2025_06_16::SETTINGS_PATTERNS,
 153            &SETTINGS_QUERY_2025_06_16,
 154        ),
 155    ];
 156    run_migrations(text, migrations)
 157}
 158
 159pub fn migrate_edit_prediction_provider_settings(text: &str) -> Result<Option<String>> {
 160    migrate(
 161        &text,
 162        &[(
 163            SETTINGS_NESTED_KEY_VALUE_PATTERN,
 164            migrations::m_2025_01_29::replace_edit_prediction_provider_setting,
 165        )],
 166        &EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY,
 167    )
 168}
 169
 170pub type MigrationPatterns = &'static [(
 171    &'static str,
 172    fn(&str, &QueryMatch, &Query) -> Option<(Range<usize>, String)>,
 173)];
 174
 175macro_rules! define_query {
 176    ($var_name:ident, $patterns_path:path) => {
 177        static $var_name: LazyLock<Query> = LazyLock::new(|| {
 178            Query::new(
 179                &tree_sitter_json::LANGUAGE.into(),
 180                &$patterns_path
 181                    .iter()
 182                    .map(|pattern| pattern.0)
 183                    .collect::<String>(),
 184            )
 185            .unwrap()
 186        });
 187    };
 188}
 189
 190// keymap
 191define_query!(
 192    KEYMAP_QUERY_2025_01_29,
 193    migrations::m_2025_01_29::KEYMAP_PATTERNS
 194);
 195define_query!(
 196    KEYMAP_QUERY_2025_01_30,
 197    migrations::m_2025_01_30::KEYMAP_PATTERNS
 198);
 199define_query!(
 200    KEYMAP_QUERY_2025_03_03,
 201    migrations::m_2025_03_03::KEYMAP_PATTERNS
 202);
 203define_query!(
 204    KEYMAP_QUERY_2025_03_06,
 205    migrations::m_2025_03_06::KEYMAP_PATTERNS
 206);
 207define_query!(
 208    KEYMAP_QUERY_2025_04_15,
 209    migrations::m_2025_04_15::KEYMAP_PATTERNS
 210);
 211
 212// settings
 213define_query!(
 214    SETTINGS_QUERY_2025_01_02,
 215    migrations::m_2025_01_02::SETTINGS_PATTERNS
 216);
 217define_query!(
 218    SETTINGS_QUERY_2025_01_29,
 219    migrations::m_2025_01_29::SETTINGS_PATTERNS
 220);
 221define_query!(
 222    SETTINGS_QUERY_2025_01_30,
 223    migrations::m_2025_01_30::SETTINGS_PATTERNS
 224);
 225define_query!(
 226    SETTINGS_QUERY_2025_03_29,
 227    migrations::m_2025_03_29::SETTINGS_PATTERNS
 228);
 229define_query!(
 230    SETTINGS_QUERY_2025_04_15,
 231    migrations::m_2025_04_15::SETTINGS_PATTERNS
 232);
 233define_query!(
 234    SETTINGS_QUERY_2025_04_21,
 235    migrations::m_2025_04_21::SETTINGS_PATTERNS
 236);
 237define_query!(
 238    SETTINGS_QUERY_2025_04_23,
 239    migrations::m_2025_04_23::SETTINGS_PATTERNS
 240);
 241define_query!(
 242    SETTINGS_QUERY_2025_05_05,
 243    migrations::m_2025_05_05::SETTINGS_PATTERNS
 244);
 245define_query!(
 246    SETTINGS_QUERY_2025_05_08,
 247    migrations::m_2025_05_08::SETTINGS_PATTERNS
 248);
 249define_query!(
 250    SETTINGS_QUERY_2025_05_29,
 251    migrations::m_2025_05_29::SETTINGS_PATTERNS
 252);
 253define_query!(
 254    SETTINGS_QUERY_2025_06_16,
 255    migrations::m_2025_06_16::SETTINGS_PATTERNS
 256);
 257
 258// custom query
 259static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
 260    Query::new(
 261        &tree_sitter_json::LANGUAGE.into(),
 262        SETTINGS_NESTED_KEY_VALUE_PATTERN,
 263    )
 264    .unwrap()
 265});
 266
 267#[cfg(test)]
 268mod tests {
 269    use super::*;
 270
 271    fn assert_migrate_keymap(input: &str, output: Option<&str>) {
 272        let migrated = migrate_keymap(&input).unwrap();
 273        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 274    }
 275
 276    fn assert_migrate_settings(input: &str, output: Option<&str>) {
 277        let migrated = migrate_settings(&input).unwrap();
 278        pretty_assertions::assert_eq!(migrated.as_deref(), output);
 279    }
 280
 281    #[test]
 282    fn test_replace_array_with_single_string() {
 283        assert_migrate_keymap(
 284            r#"
 285            [
 286                {
 287                    "bindings": {
 288                        "cmd-1": ["workspace::ActivatePaneInDirection", "Up"]
 289                    }
 290                }
 291            ]
 292            "#,
 293            Some(
 294                r#"
 295            [
 296                {
 297                    "bindings": {
 298                        "cmd-1": "workspace::ActivatePaneUp"
 299                    }
 300                }
 301            ]
 302            "#,
 303            ),
 304        )
 305    }
 306
 307    #[test]
 308    fn test_replace_action_argument_object_with_single_value() {
 309        assert_migrate_keymap(
 310            r#"
 311            [
 312                {
 313                    "bindings": {
 314                        "cmd-1": ["editor::FoldAtLevel", { "level": 1 }]
 315                    }
 316                }
 317            ]
 318            "#,
 319            Some(
 320                r#"
 321            [
 322                {
 323                    "bindings": {
 324                        "cmd-1": ["editor::FoldAtLevel", 1]
 325                    }
 326                }
 327            ]
 328            "#,
 329            ),
 330        )
 331    }
 332
 333    #[test]
 334    fn test_replace_action_argument_object_with_single_value_2() {
 335        assert_migrate_keymap(
 336            r#"
 337            [
 338                {
 339                    "bindings": {
 340                        "cmd-1": ["vim::PushOperator", { "Object": { "some" : "value" } }]
 341                    }
 342                }
 343            ]
 344            "#,
 345            Some(
 346                r#"
 347            [
 348                {
 349                    "bindings": {
 350                        "cmd-1": ["vim::PushObject", { "some" : "value" }]
 351                    }
 352                }
 353            ]
 354            "#,
 355            ),
 356        )
 357    }
 358
 359    #[test]
 360    fn test_rename_string_action() {
 361        assert_migrate_keymap(
 362            r#"
 363                [
 364                    {
 365                        "bindings": {
 366                            "cmd-1": "inline_completion::ToggleMenu"
 367                        }
 368                    }
 369                ]
 370            "#,
 371            Some(
 372                r#"
 373                [
 374                    {
 375                        "bindings": {
 376                            "cmd-1": "edit_prediction::ToggleMenu"
 377                        }
 378                    }
 379                ]
 380            "#,
 381            ),
 382        )
 383    }
 384
 385    #[test]
 386    fn test_rename_context_key() {
 387        assert_migrate_keymap(
 388            r#"
 389                [
 390                    {
 391                        "context": "Editor && inline_completion && !showing_completions"
 392                    }
 393                ]
 394            "#,
 395            Some(
 396                r#"
 397                [
 398                    {
 399                        "context": "Editor && edit_prediction && !showing_completions"
 400                    }
 401                ]
 402            "#,
 403            ),
 404        )
 405    }
 406
 407    #[test]
 408    fn test_incremental_migrations() {
 409        // Here string transforms to array internally. Then, that array transforms back to string.
 410        assert_migrate_keymap(
 411            r#"
 412                [
 413                    {
 414                        "bindings": {
 415                            "ctrl-q": "editor::GoToHunk", // should remain same
 416                            "ctrl-w": "editor::GoToPrevHunk", // should rename
 417                            "ctrl-q": ["editor::GoToHunk", { "center_cursor": true }], // should transform
 418                            "ctrl-w": ["editor::GoToPreviousHunk", { "center_cursor": true }] // should transform
 419                        }
 420                    }
 421                ]
 422            "#,
 423            Some(
 424                r#"
 425                [
 426                    {
 427                        "bindings": {
 428                            "ctrl-q": "editor::GoToHunk", // should remain same
 429                            "ctrl-w": "editor::GoToPreviousHunk", // should rename
 430                            "ctrl-q": "editor::GoToHunk", // should transform
 431                            "ctrl-w": "editor::GoToPreviousHunk" // should transform
 432                        }
 433                    }
 434                ]
 435            "#,
 436            ),
 437        )
 438    }
 439
 440    #[test]
 441    fn test_action_argument_snake_case() {
 442        // First performs transformations, then replacements
 443        assert_migrate_keymap(
 444            r#"
 445            [
 446                {
 447                    "bindings": {
 448                        "cmd-1": ["vim::PushOperator", { "Object": { "around": false } }],
 449                        "cmd-3": ["pane::CloseActiveItem", { "saveIntent": "saveAll" }],
 450                        "cmd-2": ["vim::NextWordStart", { "ignorePunctuation": true }],
 451                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 452                    }
 453                }
 454            ]
 455            "#,
 456            Some(
 457                r#"
 458            [
 459                {
 460                    "bindings": {
 461                        "cmd-1": ["vim::PushObject", { "around": false }],
 462                        "cmd-3": ["pane::CloseActiveItem", { "save_intent": "save_all" }],
 463                        "cmd-2": ["vim::NextWordStart", { "ignore_punctuation": true }],
 464                        "cmd-4": ["task::Spawn", { "task_name": "a b" }] // should remain as it is
 465                    }
 466                }
 467            ]
 468            "#,
 469            ),
 470        )
 471    }
 472
 473    #[test]
 474    fn test_replace_setting_name() {
 475        assert_migrate_settings(
 476            r#"
 477                {
 478                    "show_inline_completions_in_menu": true,
 479                    "show_inline_completions": true,
 480                    "inline_completions_disabled_in": ["string"],
 481                    "inline_completions": { "some" : "value" }
 482                }
 483            "#,
 484            Some(
 485                r#"
 486                {
 487                    "show_edit_predictions_in_menu": true,
 488                    "show_edit_predictions": true,
 489                    "edit_predictions_disabled_in": ["string"],
 490                    "edit_predictions": { "some" : "value" }
 491                }
 492            "#,
 493            ),
 494        )
 495    }
 496
 497    #[test]
 498    fn test_nested_string_replace_for_settings() {
 499        assert_migrate_settings(
 500            r#"
 501                {
 502                    "features": {
 503                        "inline_completion_provider": "zed"
 504                    },
 505                }
 506            "#,
 507            Some(
 508                r#"
 509                {
 510                    "features": {
 511                        "edit_prediction_provider": "zed"
 512                    },
 513                }
 514            "#,
 515            ),
 516        )
 517    }
 518
 519    #[test]
 520    fn test_replace_settings_in_languages() {
 521        assert_migrate_settings(
 522            r#"
 523                {
 524                    "languages": {
 525                        "Astro": {
 526                            "show_inline_completions": true
 527                        }
 528                    }
 529                }
 530            "#,
 531            Some(
 532                r#"
 533                {
 534                    "languages": {
 535                        "Astro": {
 536                            "show_edit_predictions": true
 537                        }
 538                    }
 539                }
 540            "#,
 541            ),
 542        )
 543    }
 544
 545    #[test]
 546    fn test_replace_settings_value() {
 547        assert_migrate_settings(
 548            r#"
 549                {
 550                    "scrollbar": {
 551                        "diagnostics": true
 552                    },
 553                    "chat_panel": {
 554                        "button": true
 555                    }
 556                }
 557            "#,
 558            Some(
 559                r#"
 560                {
 561                    "scrollbar": {
 562                        "diagnostics": "all"
 563                    },
 564                    "chat_panel": {
 565                        "button": "always"
 566                    }
 567                }
 568            "#,
 569            ),
 570        )
 571    }
 572
 573    #[test]
 574    fn test_replace_settings_name_and_value() {
 575        assert_migrate_settings(
 576            r#"
 577                {
 578                    "tabs": {
 579                        "always_show_close_button": true
 580                    }
 581                }
 582            "#,
 583            Some(
 584                r#"
 585                {
 586                    "tabs": {
 587                        "show_close_button": "always"
 588                    }
 589                }
 590            "#,
 591            ),
 592        )
 593    }
 594
 595    #[test]
 596    fn test_replace_bash_with_terminal_in_profiles() {
 597        assert_migrate_settings(
 598            r#"
 599                {
 600                    "assistant": {
 601                        "profiles": {
 602                            "custom": {
 603                                "name": "Custom",
 604                                "tools": {
 605                                    "bash": true,
 606                                    "diagnostics": true
 607                                }
 608                            }
 609                        }
 610                    }
 611                }
 612            "#,
 613            Some(
 614                r#"
 615                {
 616                    "agent": {
 617                        "profiles": {
 618                            "custom": {
 619                                "name": "Custom",
 620                                "tools": {
 621                                    "terminal": true,
 622                                    "diagnostics": true
 623                                }
 624                            }
 625                        }
 626                    }
 627                }
 628            "#,
 629            ),
 630        )
 631    }
 632
 633    #[test]
 634    fn test_replace_bash_false_with_terminal_in_profiles() {
 635        assert_migrate_settings(
 636            r#"
 637                {
 638                    "assistant": {
 639                        "profiles": {
 640                            "custom": {
 641                                "name": "Custom",
 642                                "tools": {
 643                                    "bash": false,
 644                                    "diagnostics": true
 645                                }
 646                            }
 647                        }
 648                    }
 649                }
 650            "#,
 651            Some(
 652                r#"
 653                {
 654                    "agent": {
 655                        "profiles": {
 656                            "custom": {
 657                                "name": "Custom",
 658                                "tools": {
 659                                    "terminal": false,
 660                                    "diagnostics": true
 661                                }
 662                            }
 663                        }
 664                    }
 665                }
 666            "#,
 667            ),
 668        )
 669    }
 670
 671    #[test]
 672    fn test_no_bash_in_profiles() {
 673        assert_migrate_settings(
 674            r#"
 675                {
 676                    "assistant": {
 677                        "profiles": {
 678                            "custom": {
 679                                "name": "Custom",
 680                                "tools": {
 681                                    "diagnostics": true,
 682                                    "find_path": true,
 683                                    "read_file": true
 684                                }
 685                            }
 686                        }
 687                    }
 688                }
 689            "#,
 690            Some(
 691                r#"
 692                {
 693                    "agent": {
 694                        "profiles": {
 695                            "custom": {
 696                                "name": "Custom",
 697                                "tools": {
 698                                    "diagnostics": true,
 699                                    "find_path": true,
 700                                    "read_file": true
 701                                }
 702                            }
 703                        }
 704                    }
 705                }
 706            "#,
 707            ),
 708        )
 709    }
 710
 711    #[test]
 712    fn test_rename_path_search_to_find_path() {
 713        assert_migrate_settings(
 714            r#"
 715                {
 716                    "assistant": {
 717                        "profiles": {
 718                            "default": {
 719                                "tools": {
 720                                    "path_search": true,
 721                                    "read_file": true
 722                                }
 723                            }
 724                        }
 725                    }
 726                }
 727            "#,
 728            Some(
 729                r#"
 730                {
 731                    "agent": {
 732                        "profiles": {
 733                            "default": {
 734                                "tools": {
 735                                    "find_path": true,
 736                                    "read_file": true
 737                                }
 738                            }
 739                        }
 740                    }
 741                }
 742            "#,
 743            ),
 744        );
 745    }
 746
 747    #[test]
 748    fn test_rename_assistant() {
 749        assert_migrate_settings(
 750            r#"{
 751                "assistant": {
 752                    "foo": "bar"
 753                },
 754                "edit_predictions": {
 755                    "enabled_in_assistant": false,
 756                }
 757            }"#,
 758            Some(
 759                r#"{
 760                "agent": {
 761                    "foo": "bar"
 762                },
 763                "edit_predictions": {
 764                    "enabled_in_text_threads": false,
 765                }
 766            }"#,
 767            ),
 768        );
 769    }
 770
 771    #[test]
 772    fn test_comment_duplicated_agent() {
 773        assert_migrate_settings(
 774            r#"{
 775                "agent": {
 776                    "name": "assistant-1",
 777                "model": "gpt-4", // weird formatting
 778                    "utf8": "привіт"
 779                },
 780                "something": "else",
 781                "agent": {
 782                    "name": "assistant-2",
 783                    "model": "gemini-pro"
 784                }
 785            }
 786        "#,
 787            Some(
 788                r#"{
 789                /* Duplicated key auto-commented: "agent": {
 790                    "name": "assistant-1",
 791                "model": "gpt-4", // weird formatting
 792                    "utf8": "привіт"
 793                }, */
 794                "something": "else",
 795                "agent": {
 796                    "name": "assistant-2",
 797                    "model": "gemini-pro"
 798                }
 799            }
 800        "#,
 801            ),
 802        );
 803    }
 804
 805    #[test]
 806    fn test_preferred_completion_mode_migration() {
 807        assert_migrate_settings(
 808            r#"{
 809                "agent": {
 810                    "preferred_completion_mode": "max",
 811                    "enabled": true
 812                }
 813            }"#,
 814            Some(
 815                r#"{
 816                "agent": {
 817                    "preferred_completion_mode": "burn",
 818                    "enabled": true
 819                }
 820            }"#,
 821            ),
 822        );
 823
 824        assert_migrate_settings(
 825            r#"{
 826                "agent": {
 827                    "preferred_completion_mode": "normal",
 828                    "enabled": true
 829                }
 830            }"#,
 831            None,
 832        );
 833
 834        assert_migrate_settings(
 835            r#"{
 836                "agent": {
 837                    "preferred_completion_mode": "burn",
 838                    "enabled": true
 839                }
 840            }"#,
 841            None,
 842        );
 843
 844        assert_migrate_settings(
 845            r#"{
 846                "other_section": {
 847                    "preferred_completion_mode": "max"
 848                },
 849                "agent": {
 850                    "preferred_completion_mode": "max"
 851                }
 852            }"#,
 853            Some(
 854                r#"{
 855                "other_section": {
 856                    "preferred_completion_mode": "max"
 857                },
 858                "agent": {
 859                    "preferred_completion_mode": "burn"
 860                }
 861            }"#,
 862            ),
 863        );
 864    }
 865
 866    #[test]
 867    fn test_mcp_settings_migration() {
 868        assert_migrate_settings(
 869            r#"{
 870    "context_servers": {
 871        "empty_server": {},
 872        "extension_server": {
 873            "settings": {
 874                "foo": "bar"
 875            }
 876        },
 877        "custom_server": {
 878            "command": {
 879                "path": "foo",
 880                "args": ["bar"],
 881                "env": {
 882                    "FOO": "BAR"
 883                }
 884            }
 885        },
 886        "invalid_server": {
 887            "command": {
 888                "path": "foo",
 889                "args": ["bar"],
 890                "env": {
 891                    "FOO": "BAR"
 892                }
 893            },
 894            "settings": {
 895                "foo": "bar"
 896            }
 897        },
 898        "empty_server2": {},
 899        "extension_server2": {
 900            "foo": "bar",
 901            "settings": {
 902                "foo": "bar"
 903            },
 904            "bar": "foo"
 905        },
 906        "custom_server2": {
 907            "foo": "bar",
 908            "command": {
 909                "path": "foo",
 910                "args": ["bar"],
 911                "env": {
 912                    "FOO": "BAR"
 913                }
 914            },
 915            "bar": "foo"
 916        },
 917        "invalid_server2": {
 918            "foo": "bar",
 919            "command": {
 920                "path": "foo",
 921                "args": ["bar"],
 922                "env": {
 923                    "FOO": "BAR"
 924                }
 925            },
 926            "bar": "foo",
 927            "settings": {
 928                "foo": "bar"
 929            }
 930        }
 931    }
 932}"#,
 933            Some(
 934                r#"{
 935    "context_servers": {
 936        "empty_server": {
 937            "source": "extension",
 938            "settings": {}
 939        },
 940        "extension_server": {
 941            "source": "extension",
 942            "settings": {
 943                "foo": "bar"
 944            }
 945        },
 946        "custom_server": {
 947            "source": "custom",
 948            "command": {
 949                "path": "foo",
 950                "args": ["bar"],
 951                "env": {
 952                    "FOO": "BAR"
 953                }
 954            }
 955        },
 956        "invalid_server": {
 957            "source": "custom",
 958            "command": {
 959                "path": "foo",
 960                "args": ["bar"],
 961                "env": {
 962                    "FOO": "BAR"
 963                }
 964            },
 965            "settings": {
 966                "foo": "bar"
 967            }
 968        },
 969        "empty_server2": {
 970            "source": "extension",
 971            "settings": {}
 972        },
 973        "extension_server2": {
 974            "source": "extension",
 975            "foo": "bar",
 976            "settings": {
 977                "foo": "bar"
 978            },
 979            "bar": "foo"
 980        },
 981        "custom_server2": {
 982            "source": "custom",
 983            "foo": "bar",
 984            "command": {
 985                "path": "foo",
 986                "args": ["bar"],
 987                "env": {
 988                    "FOO": "BAR"
 989                }
 990            },
 991            "bar": "foo"
 992        },
 993        "invalid_server2": {
 994            "source": "custom",
 995            "foo": "bar",
 996            "command": {
 997                "path": "foo",
 998                "args": ["bar"],
 999                "env": {
1000                    "FOO": "BAR"
1001                }
1002            },
1003            "bar": "foo",
1004            "settings": {
1005                "foo": "bar"
1006            }
1007        }
1008    }
1009}"#,
1010            ),
1011        );
1012    }
1013
1014    #[test]
1015    fn test_mcp_settings_migration_doesnt_change_valid_settings() {
1016        let settings = r#"{
1017    "context_servers": {
1018        "empty_server": {
1019            "source": "extension",
1020            "settings": {}
1021        },
1022        "extension_server": {
1023            "source": "extension",
1024            "settings": {
1025                "foo": "bar"
1026            }
1027        },
1028        "custom_server": {
1029            "source": "custom",
1030            "command": {
1031                "path": "foo",
1032                "args": ["bar"],
1033                "env": {
1034                    "FOO": "BAR"
1035                }
1036            }
1037        },
1038        "invalid_server": {
1039            "source": "custom",
1040            "command": {
1041                "path": "foo",
1042                "args": ["bar"],
1043                "env": {
1044                    "FOO": "BAR"
1045                }
1046            },
1047            "settings": {
1048                "foo": "bar"
1049            }
1050        }
1051    }
1052}"#;
1053        assert_migrate_settings(settings, None);
1054    }
1055}