fix(profiles): reject non-string profile names instead of coercing

A numeric profile id (DB row id or falsy sentinel) handed to
normalize_profile_name() was silently str()'d into a real profile name:
`0` → `profiles/0/` bootstrap with no config.yaml (phantom profile
created on desktop first launch, #88842).

normalize_profile_name now raises ValueError for non-string input;
callers holding a numeric id must resolve it to an actual profile name
first. A literal "0" typed by a user remains a legal explicit name.
This commit is contained in:
SayHell0W0rld
2026-08-19 10:51:35 +08:00
committed by brooklyn!
parent e78646a380
commit dcbc15252c
2 changed files with 28 additions and 2 deletions

View File

@@ -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/<id>/``. See #18498.
Named profiles are stored lowercase under ``profiles/<id>/``. 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")

View File

@@ -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()."""