hermes-agent/tests/hermes_cli/test_profile_install_env_encoding.py
solyanviktor-star 40828997a5 fix(profile): read .env as utf-8-sig in the distribution-install preview
`_render_distribution_plan` reads the target profile's `.env` to decide
whether a required env var is already set (so it doesn't nag the user),
using `Path.read_text()` with no encoding. Two bugs:

1. `Path.read_text()` defaults to the system locale (cp1251/GBK on Windows),
   which raises `UnicodeDecodeError` on any non-ASCII byte. The surrounding
   `except OSError` does NOT catch that — `UnicodeDecodeError` is a
   `ValueError` — so a mis-encoded `.env` aborts the entire install preview.
2. Even on a UTF-8 locale, a Notepad-added BOM prefixes the first key
   (`KEY`), so the very first required env var is mis-reported as
   "needs setting" when it is actually present.

`.env` is written as UTF-8 everywhere in the codebase. Read it as
`utf-8-sig` (tolerates the BOM) and also catch `UnicodeDecodeError` so a
genuinely un-decodable file skips the pre-check instead of crashing.

Regression tests: a BOM-prefixed `.env` whose first key must still read as
"set", and an invalid-UTF-8 `.env` that must not abort the preview.
2026-07-24 17:10:39 -07:00

72 lines
2.7 KiB
Python

"""Regression: the distribution-install preview must read .env as UTF-8.
`_render_distribution_plan` inspects the target profile's `.env` to decide
whether a required env var is already set, so it doesn't nag the user. It
used `Path.read_text()` with no encoding, which:
1. defaults to the system locale (cp1251/GBK on Windows) and raises
`UnicodeDecodeError` on any non-ASCII byte — and the surrounding
`except OSError` does NOT catch that (it's a `ValueError`), so a
mis-encoded .env aborts the whole install preview; and
2. even on a UTF-8 locale, a Notepad-added BOM prefixes the first key,
so the very first env var is mis-detected as "needs setting".
The fix reads `utf-8-sig` and also catches `UnicodeDecodeError`.
"""
from types import SimpleNamespace
import pytest
from hermes_cli.main import _render_distribution_plan
def _make_plan(target_dir, env_requires):
manifest = SimpleNamespace(
name="demo",
version="1.0.0",
description="",
author="",
hermes_requires="",
env_requires=env_requires,
)
return SimpleNamespace(
manifest=manifest,
provenance="local",
target_dir=target_dir,
existing=False,
has_cron=False,
)
def test_bom_prefixed_env_first_key_is_detected_as_set(tmp_path, monkeypatch, capsys):
"""A required key on the first line of a BOM-prefixed .env reads as set."""
monkeypatch.delenv("FOO_TOKEN", raising=False)
profile = tmp_path / "profile"
profile.mkdir()
# utf-8-sig write == UTF-8 with a leading BOM, exactly what Notepad emits.
(profile / ".env").write_text("FOO_TOKEN=abc\nBAR=1\n", encoding="utf-8-sig")
plan = _make_plan(profile, [SimpleNamespace(name="FOO_TOKEN", required=True, description="")])
_render_distribution_plan(plan)
out = capsys.readouterr().out
foo_line = next(ln for ln in out.splitlines() if "FOO_TOKEN" in ln)
assert "✓ set" in foo_line, f"BOM must not hide the first key: {foo_line!r}"
def test_non_utf8_env_does_not_abort_the_preview(tmp_path, monkeypatch, capsys):
"""A .env with invalid UTF-8 bytes must not crash the install preview."""
monkeypatch.delenv("FOO_TOKEN", raising=False)
profile = tmp_path / "profile"
profile.mkdir()
# \xff is an invalid UTF-8 start byte -> UnicodeDecodeError on read.
(profile / ".env").write_bytes(b"FOO_TOKEN=\xff\xfe bad\n")
plan = _make_plan(profile, [SimpleNamespace(name="FOO_TOKEN", required=True, description="")])
# Must return normally (old `except OSError` let UnicodeDecodeError escape).
_render_distribution_plan(plan)
out = capsys.readouterr().out
assert any("FOO_TOKEN" in ln for ln in out.splitlines())