From 90ef5e457520ffbd17f8bb073bfe882e849214de Mon Sep 17 00:00:00 2001 From: duyua9 Date: Fri, 8 May 2026 21:21:00 +0800 Subject: [PATCH] fix(cli): tolerate BOM in ov config --- openviking_cli/doctor.py | 4 +-- .../utils/config/open_viking_config.py | 2 +- tests/cli/test_doctor.py | 8 ++++++ tests/test_config_loader.py | 28 +++++++++++++++++++ 4 files changed, 39 insertions(+), 3 deletions(-) diff --git a/openviking_cli/doctor.py b/openviking_cli/doctor.py index 8a21f5e71..849fffb1c 100644 --- a/openviking_cli/doctor.py +++ b/openviking_cli/doctor.py @@ -54,7 +54,7 @@ def _find_config() -> Optional[Path]: def _load_config_json(config_path: Path) -> Optional[dict]: """Parse ov.conf as JSON. Returns None if the file is unreadable or not valid JSON.""" try: - raw = config_path.read_text(encoding="utf-8") + raw = config_path.read_text(encoding="utf-8-sig") raw = os.path.expandvars(raw) return json.loads(raw) except (OSError, json.JSONDecodeError): @@ -72,7 +72,7 @@ def check_config() -> tuple[bool, str, Optional[str]]: ) try: - raw = config_path.read_text(encoding="utf-8") + raw = config_path.read_text(encoding="utf-8-sig") raw = os.path.expandvars(raw) data = json.loads(raw) except json.JSONDecodeError as exc: diff --git a/openviking_cli/utils/config/open_viking_config.py b/openviking_cli/utils/config/open_viking_config.py index 9ebaa905b..25076e77b 100644 --- a/openviking_cli/utils/config/open_viking_config.py +++ b/openviking_cli/utils/config/open_viking_config.py @@ -388,7 +388,7 @@ def _load_from_file(cls, config_file: str) -> "OpenVikingConfig": if not config_path.exists(): raise FileNotFoundError(f"Config file does not exist: {config_file}") - with open(config_path, "r", encoding="utf-8") as f: + with open(config_path, "r", encoding="utf-8-sig") as f: raw = f.read() # Expand $VAR and ${VAR} inside the JSON text (useful for container deployments). diff --git a/tests/cli/test_doctor.py b/tests/cli/test_doctor.py index 922829d87..43e30f642 100644 --- a/tests/cli/test_doctor.py +++ b/tests/cli/test_doctor.py @@ -32,6 +32,14 @@ def test_pass_with_valid_config(self, tmp_path: Path): assert ok assert str(config) in detail + def test_pass_with_utf8_bom_config(self, tmp_path: Path): + config = tmp_path / "ov.conf" + config.write_text("\ufeff" + json.dumps({"embedding": {"dense": {}}})) + with patch("openviking_cli.doctor._find_config", return_value=config): + ok, detail, fix = check_config() + assert ok + assert str(config) in detail + def test_fail_missing_config(self): with patch("openviking_cli.doctor._find_config", return_value=None): ok, detail, fix = check_config() diff --git a/tests/test_config_loader.py b/tests/test_config_loader.py index a9ed1455d..ea0b7df05 100644 --- a/tests/test_config_loader.py +++ b/tests/test_config_loader.py @@ -233,6 +233,34 @@ def test_openviking_config_singleton_preserves_value_error_for_bad_config(tmp_pa OpenVikingConfigSingleton.reset_instance() +def test_openviking_config_singleton_loads_utf8_bom_config(tmp_path, monkeypatch): + monkeypatch.setenv(OPENVIKING_CONFIG_ENV, "/tmp/codex-no-config.json") + + from openviking_cli.utils.config import open_viking_config as config_module + + class _ConfigStub: + default_account = "default" + + loaded = {} + + def _from_dict(data): + loaded.update(data) + return _ConfigStub() + + monkeypatch.setattr(config_module.OpenVikingConfig, "from_dict", _from_dict) + + config_path = tmp_path / "ov.conf" + config_path.write_text("\ufeff{}", encoding="utf-8") + + config_module.OpenVikingConfigSingleton.reset_instance() + config = config_module.OpenVikingConfigSingleton.initialize(config_path=str(config_path)) + + assert config.default_account == "default" + assert loaded == {} + + config_module.OpenVikingConfigSingleton.reset_instance() + + def test_require_config_missing_message_uses_openviking_ai_docs(tmp_path, monkeypatch): import openviking_cli.utils.config.config_loader as loader