refactor(profiles): align profile cookie env var with HERMES_WEBUI_* naming

The profile cookie has been configurable since #1756 via
WEBUI_PROFILE_COOKIE_NAME, the lone WebUI env var missing the HERMES_WEBUI_
prefix shared by every other setting (e.g. HERMES_WEBUI_COOKIE_NAME from #3981).

- Read HERMES_WEBUI_PROFILE_COOKIE_NAME first (canonical name)
- Keep WEBUI_PROFILE_COOKIE_NAME as a deprecated alias so existing deployments
  are unaffected; behavior is unchanged, only the name is aligned
- Warn once per process for the legacy name (this resolver runs on every
  request, so the deprecation log must not fire per-request)
- Add resolution tests covering canonical, legacy, precedence, blank, and
  warn-once paths
This commit is contained in:
gaku
2026-06-12 03:39:43 +00:00
parent df6cfd0a15
commit 6a13feaf8b
2 changed files with 82 additions and 2 deletions

View File

@@ -495,11 +495,36 @@ def read_body(handler) -> dict:
# ── Profile cookie helpers (issue #798) ─────────────────────────────────────
PROFILE_COOKIE_NAME = 'hermes_profile'
_PROFILE_COOKIE_ENV = 'HERMES_WEBUI_PROFILE_COOKIE_NAME'
_LEGACY_PROFILE_COOKIE_ENV = 'WEBUI_PROFILE_COOKIE_NAME'
_legacy_profile_cookie_warned = False
def get_profile_cookie_name() -> str:
"""Return the cookie name used to persist the active WebUI profile."""
return os.getenv('WEBUI_PROFILE_COOKIE_NAME', PROFILE_COOKIE_NAME)
"""Return the cookie name used to persist the active WebUI profile.
Honours ``HERMES_WEBUI_PROFILE_COOKIE_NAME`` so multiple WebUI instances
sharing a hostname (different ports) can use distinct profile-cookie names
instead of trampling each other; browsers scope cookies by host, not
host+port (RFC 6265). The original ``WEBUI_PROFILE_COOKIE_NAME`` is still
honoured as a deprecated fallback (warned once per process, since this is
called on every request).
"""
name = os.getenv(_PROFILE_COOKIE_ENV, '').strip()
if name:
return name
legacy = os.getenv(_LEGACY_PROFILE_COOKIE_ENV, '').strip()
if legacy:
global _legacy_profile_cookie_warned
if not _legacy_profile_cookie_warned:
logger.warning(
'%s is deprecated; use %s instead.',
_LEGACY_PROFILE_COOKIE_ENV,
_PROFILE_COOKIE_ENV,
)
_legacy_profile_cookie_warned = True
return legacy
return PROFILE_COOKIE_NAME
def get_profile_cookie(handler) -> str | None:

View File

@@ -13,6 +13,7 @@ Covers:
4. switch_profile(process_wide=False) does NOT mutate process globals
5. Concurrent requests on different threads see independent profiles
"""
import logging
import os
import threading
from pathlib import Path
@@ -230,6 +231,60 @@ class TestProfileCookieHelpers:
assert get_profile_cookie(handler) is None
# ── 1b. Profile cookie name resolution (env > legacy env > default) ───────────
class TestProfileCookieNameResolution:
def test_default_when_unset(self, monkeypatch):
from api.helpers import PROFILE_COOKIE_NAME, get_profile_cookie_name
monkeypatch.delenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', raising=False)
monkeypatch.delenv('WEBUI_PROFILE_COOKIE_NAME', raising=False)
assert get_profile_cookie_name() == PROFILE_COOKIE_NAME
def test_canonical_env_overrides_default(self, monkeypatch):
from api.helpers import get_profile_cookie_name
monkeypatch.delenv('WEBUI_PROFILE_COOKIE_NAME', raising=False)
monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_alt')
assert get_profile_cookie_name() == 'hermes_profile_alt'
def test_legacy_env_still_honoured(self, monkeypatch):
from api.helpers import get_profile_cookie_name
monkeypatch.delenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', raising=False)
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_legacy')
assert get_profile_cookie_name() == 'hermes_profile_legacy'
def test_canonical_takes_precedence_over_legacy(self, monkeypatch):
from api.helpers import get_profile_cookie_name
monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', 'canonical')
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'legacy')
assert get_profile_cookie_name() == 'canonical'
def test_blank_canonical_falls_back_to_legacy(self, monkeypatch):
from api.helpers import get_profile_cookie_name
monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', ' ')
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_legacy')
assert get_profile_cookie_name() == 'hermes_profile_legacy'
def test_blank_envs_fall_back_to_default(self, monkeypatch):
from api.helpers import PROFILE_COOKIE_NAME, get_profile_cookie_name
monkeypatch.setenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', ' ')
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', '')
assert get_profile_cookie_name() == PROFILE_COOKIE_NAME
def test_legacy_deprecation_warns_only_once(self, monkeypatch, caplog):
# get_profile_cookie_name() runs on every request, so the deprecation
# warning for the legacy env var must be emitted once per process.
import api.helpers as helpers
monkeypatch.delenv('HERMES_WEBUI_PROFILE_COOKIE_NAME', raising=False)
monkeypatch.setenv('WEBUI_PROFILE_COOKIE_NAME', 'hermes_profile_legacy')
monkeypatch.setattr(helpers, '_legacy_profile_cookie_warned', False)
with caplog.at_level(logging.WARNING, logger='api.helpers'):
for _ in range(3):
assert helpers.get_profile_cookie_name() == 'hermes_profile_legacy'
warned = [r for r in caplog.records if 'deprecated' in r.getMessage()]
assert len(warned) == 1
# ── 2. Thread-local request context ──────────────────────────────────────────
class TestThreadLocalProfileContext: