fix(cli): warn once per path for UTF-32 .env refuse-to-mangle

Hot-reload and multi-entry load_hermes_dotenv can hit the same UTF-32
file repeatedly; gate the refuse-to-mangle warning on a module-level
seen-set (house style: _WARNED_KEYS sibling) so logs are not spammed.
This commit is contained in:
Paulo Nascimento 2026-07-17 17:33:59 -04:00 committed by Brooklyn Nicholson
parent 7d597cc5d4
commit 90d3ba5be9
2 changed files with 44 additions and 6 deletions

View file

@ -23,6 +23,12 @@ _CREDENTIAL_SUFFIXES = ("_API_KEY", "_TOKEN", "_SECRET", "_KEY")
# tests) don't spam the same warning multiple times.
_WARNED_KEYS: set[str] = set()
# Paths we've already emitted a UTF-32 refuse-to-mangle warning for.
# load_hermes_dotenv can call _sanitize_env_file_if_needed multiple times
# for the same file (user env + project env + hot-reload); once per path
# is enough.
_WARNED_UTF32_PATHS: set[str] = set()
# Map of env-var name → source label ("bitwarden", etc.) for credentials
# that were injected by an external secret source during load_hermes_dotenv().
# Used by setup / `hermes model` flows to label detected credentials so
@ -208,13 +214,16 @@ def _sanitize_env_file_if_needed(path: Path) -> None:
if raw.startswith(codecs.BOM_UTF32_LE) or raw.startswith(codecs.BOM_UTF32_BE):
# Lazy import keeps the module import block identical to #65124's
# codecs/io additions so the two PRs auto-merge either order.
import logging
path_key = str(path.resolve())
if path_key not in _WARNED_UTF32_PATHS:
_WARNED_UTF32_PATHS.add(path_key)
import logging
logging.getLogger(__name__).warning(
"Skipping .env sanitize for %s: UTF-32 BOM detected; "
"leaving file untouched to avoid corruption",
path,
)
logging.getLogger(__name__).warning(
"Skipping .env sanitize for %s: UTF-32 BOM detected; "
"leaving file untouched to avoid corruption",
path,
)
return
if raw.startswith(codecs.BOM_UTF16_LE) or raw.startswith(codecs.BOM_UTF16_BE):
# "utf-16" uses the BOM to select endianness and strips it.

View file

@ -276,6 +276,35 @@ def test_utf32_be_bom_leaves_file_untouched(tmp_path, caplog):
assert any("UTF-32" in r.message for r in caplog.records)
def test_utf32_warning_fires_once_per_path(tmp_path, caplog, monkeypatch):
"""Three sanitize calls on the same UTF-32 file → exactly one warning.
Matches house style for warn-once (module-level seen-set, same class as
``_WARNED_KEYS``): hot-reload / multi-entry load must not spam logs.
"""
import logging
import hermes_cli.env_loader as env_loader
from hermes_cli.env_loader import _sanitize_env_file_if_needed
# Isolate process-level seen-set so other tests' paths don't leak in.
monkeypatch.setattr(env_loader, "_WARNED_UTF32_PATHS", set())
env_file = tmp_path / ".env"
content = "HERMES_TEST_KEY=hello_utf32\nSECOND_KEY=world\n"
raw = codecs.BOM_UTF32_LE + content.encode("utf-32-le")
env_file.write_bytes(raw)
with caplog.at_level(logging.WARNING, logger="hermes_cli.env_loader"):
_sanitize_env_file_if_needed(env_file)
_sanitize_env_file_if_needed(env_file)
_sanitize_env_file_if_needed(env_file)
utf32_warnings = [r for r in caplog.records if "UTF-32" in r.message]
assert len(utf32_warnings) == 1
assert env_file.read_bytes() == raw
def test_leading_replacement_char_does_not_rewrite(tmp_path):
"""errors=replace FFFD-on-first-line guard: do not persist mangling.