diff --git a/sdks/python-cli/omi_cli/config.py b/sdks/python-cli/omi_cli/config.py index 02ccd7aef88..3da7fc0fbff 100644 --- a/sdks/python-cli/omi_cli/config.py +++ b/sdks/python-cli/omi_cli/config.py @@ -200,6 +200,14 @@ def load(path: Optional[Path] = None) -> Config: ) active = data.get("active_profile", DEFAULT_PROFILE_NAME) + if not isinstance(active, str): + return Config( + path=p, + active_profile=DEFAULT_PROFILE_NAME, + profiles={}, + load_error=f"'active_profile' must be a string, got {type(active).__name__}", + ) + profiles_data = data.get("profiles", {}) # Validate that profiles is a table (dict), not a string or other scalar. diff --git a/sdks/python-cli/tests/test_config.py b/sdks/python-cli/tests/test_config.py index cd2852a4b77..05ab25d93b7 100644 --- a/sdks/python-cli/tests/test_config.py +++ b/sdks/python-cli/tests/test_config.py @@ -415,3 +415,51 @@ def test_no_profiles_section(config_path: Path) -> None: config = cfg.load() assert not config.was_load_error assert config.active_profile == "other" + + +# -- Regression tests for non-string active_profile selector (Issue #13442) -- + + +@pytest.mark.parametrize( + "invalid_toml,expected_type", + [ + ('active_profile = ["work"]\n', "list"), + ("active_profile = 42\n", "int"), + ("active_profile = true\n", "bool"), + ("[active_profile]\nname = 'work'\n", "dict"), + ], +) +def test_active_profile_non_string_records_load_error( + config_path: Path, invalid_toml: str, expected_type: str +) -> None: + """active_profile must be a string; non-string values should set load_error instead of crashing.""" + config_path.write_text(invalid_toml, encoding="utf-8") + config = cfg.load() + assert config.was_load_error + assert config.active_profile == cfg.DEFAULT_PROFILE_NAME + assert config.profiles == {} + assert config.load_error is not None + assert f"'active_profile' must be a string, got {expected_type}" in config.load_error + + +def test_active_profile_non_string_refuses_save_overwrite(config_path: Path) -> None: + """A config with invalid active_profile type must not be overwritten by save().""" + config_path.write_text('active_profile = ["work"]\n[profiles.work]\napi_base = "https://api.omi.me"\n', encoding="utf-8") + config = cfg.load() + assert config.was_load_error + + with pytest.raises(PermissionError, match="refusing to overwrite"): + cfg.save(config) + + # The file on disk is preserved intact + assert 'active_profile = ["work"]' in config_path.read_text(encoding="utf-8") + + +def test_active_profile_non_string_diagnostics_succeed(config_path: Path, cli_runner) -> None: + """Read-only diagnostics commands must still succeed when active_profile is invalid.""" + config_path.write_text('active_profile = ["work"]\n', encoding="utf-8") + result = cli_runner.invoke(app, ["version"]) + assert result.exit_code == 0, result.output + + result_path = cli_runner.invoke(app, ["config", "path"]) + assert result_path.exit_code == 0, result_path.output