diff --git a/hermes_cli/profiles.py b/hermes_cli/profiles.py index c21b5319dd..8ee6d18850 100644 --- a/hermes_cli/profiles.py +++ b/hermes_cli/profiles.py @@ -294,10 +294,21 @@ def normalize_profile_name(name: str) -> str: case-insensitively. Dashboards/tools may pass title-cased labels — normalize before validation, assignment, and subprocess spawn. - Named profiles are stored lowercase under ``profiles//``. See #18498. + Named profiles are stored lowercase under ``profiles//``. The special + alias ``default`` is matched case-insensitively (``Default`` → ``default``). + Dashboards and tools may pass title-cased display labels; normalize before + validation, assignment, and subprocess spawn (see issue #18498). + + Raises ``ValueError`` for non-string input: a numeric profile id (e.g. a + DB row id or a falsy sentinel) silently coerced via ``str()`` becomes a + real on-disk profile directory (``profiles/0/``, #88842). Callers that + hold a numeric id must resolve it to an actual profile name first. """ if not isinstance(name, str): - name = str(name) + raise ValueError( + "profile name must be a string, got " + f"{type(name).__name__}: {name!r}" + ) stripped = name.strip() if not stripped: raise ValueError("profile name cannot be empty") diff --git a/tests/hermes_cli/test_profiles.py b/tests/hermes_cli/test_profiles.py index 035dbd8311..95281c20cc 100644 --- a/tests/hermes_cli/test_profiles.py +++ b/tests/hermes_cli/test_profiles.py @@ -75,6 +75,21 @@ class TestNormalizeProfileName: assert normalize_profile_name("Jules") == "jules" assert normalize_profile_name(" Librarian ") == "librarian" + def test_non_string_name_rejected(self): + # A numeric profile id (DB row id / falsy sentinel) must not be + # silently coerced into a real on-disk profile directory (#88842). + with pytest.raises(ValueError): + normalize_profile_name(0) + with pytest.raises(ValueError): + normalize_profile_name(None) + with pytest.raises(ValueError): + normalize_profile_name(42) + + def test_literal_zero_string_is_a_valid_explicit_name(self): + # A literal "0" typed by the user is a legal profile id; only the + # non-string coercion created the phantom profile. + assert normalize_profile_name("0") == "0" + class TestValidateProfileName: """Tests for validate_profile_name()."""