"""Tests for the YAML loader and hash verification.""" from __future__ import annotations from decimal import Decimal from pathlib import Path import pytest import yaml from cerbero_bite.config.loader import ( ConfigHashError, compute_config_hash, load_strategy, ) REPO_ROOT = Path(__file__).resolve().parents[2] # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _golden_yaml_skeleton(**overrides: object) -> dict[str, object]: base = { "config_version": "1.0.0-test", "config_hash": "0" * 64, "last_review": "2026-04-26", "last_reviewer": "test", } base.update(overrides) return base def _write_with_correct_hash(path: Path, doc: dict[str, object]) -> None: """Write a YAML doc and patch ``config_hash`` to match the file body.""" text = yaml.safe_dump(doc, sort_keys=False) path.write_text(text, encoding="utf-8") new_hash = compute_config_hash(path.read_text(encoding="utf-8")) doc = {**doc, "config_hash": new_hash} text = yaml.safe_dump(doc, sort_keys=False) path.write_text(text, encoding="utf-8") # --------------------------------------------------------------------------- # compute_config_hash # --------------------------------------------------------------------------- def test_compute_hash_is_independent_of_recorded_hash_value(tmp_path: Path) -> None: a = tmp_path / "a.yaml" b = tmp_path / "b.yaml" a.write_text( 'config_version: "1.0.0"\nconfig_hash: "aaa"\nfoo: 1\n', encoding="utf-8" ) b.write_text( 'config_version: "1.0.0"\nconfig_hash: "bbb"\nfoo: 1\n', encoding="utf-8" ) assert compute_config_hash(a.read_text()) == compute_config_hash(b.read_text()) # --------------------------------------------------------------------------- # load_strategy — happy path # --------------------------------------------------------------------------- def test_load_repo_strategy_yaml(tmp_path: Path) -> None: """The committed strategy.yaml validates with the recorded hash.""" result = load_strategy(REPO_ROOT / "strategy.yaml") assert result.config.config_version == "1.1.0" assert result.config.sizing.kelly_fraction == Decimal("0.13") assert result.computed_hash == result.config.config_hash def test_load_with_local_override_merges(tmp_path: Path) -> None: main = tmp_path / "strategy.yaml" _write_with_correct_hash(main, _golden_yaml_skeleton()) override = tmp_path / "strategy.local.yaml" override.write_text( yaml.safe_dump( {"sizing": {"max_concurrent_positions": 0}}, sort_keys=False, ), encoding="utf-8", ) loaded = load_strategy(main) assert loaded.config.sizing.max_concurrent_positions == 0 assert override in loaded.sources def test_local_override_does_not_invalidate_main_hash(tmp_path: Path) -> None: main = tmp_path / "strategy.yaml" _write_with_correct_hash(main, _golden_yaml_skeleton()) (tmp_path / "strategy.local.yaml").write_text( "sizing:\n kelly_fraction: '0.10'\n", encoding="utf-8" ) loaded = load_strategy(main) assert loaded.config.sizing.kelly_fraction == Decimal("0.10") # --------------------------------------------------------------------------- # load_strategy — error paths # --------------------------------------------------------------------------- def test_load_with_mismatched_hash_raises(tmp_path: Path) -> None: main = tmp_path / "strategy.yaml" main.write_text( yaml.safe_dump(_golden_yaml_skeleton(), sort_keys=False), encoding="utf-8" ) with pytest.raises(ConfigHashError, match="config_hash mismatch"): load_strategy(main) def test_load_with_enforce_hash_false_skips_check(tmp_path: Path) -> None: main = tmp_path / "strategy.yaml" main.write_text( yaml.safe_dump(_golden_yaml_skeleton(), sort_keys=False), encoding="utf-8" ) loaded = load_strategy(main, enforce_hash=False) assert loaded.config.config_hash == "0" * 64 def test_load_rejects_top_level_non_mapping(tmp_path: Path) -> None: main = tmp_path / "strategy.yaml" main.write_text("- a\n- b\n", encoding="utf-8") with pytest.raises(ValueError, match="top-level mapping"): load_strategy(main, enforce_hash=False) def test_local_override_path_pointing_to_missing_file_is_ignored( tmp_path: Path, ) -> None: main = tmp_path / "strategy.yaml" _write_with_correct_hash(main, _golden_yaml_skeleton()) loaded = load_strategy( main, local_override_path=tmp_path / "nonexistent.yaml" ) assert main in loaded.sources assert len(loaded.sources) == 1 def test_empty_local_override_file_is_no_op(tmp_path: Path) -> None: main = tmp_path / "strategy.yaml" _write_with_correct_hash(main, _golden_yaml_skeleton()) (tmp_path / "strategy.local.yaml").write_text("", encoding="utf-8") loaded = load_strategy(main) assert loaded.config.sizing.kelly_fraction == Decimal("0.13")