"""Tests for `write_config` — TOML serialiser used by the first-run wizard. The load-bearing invariant: write_config → load_config produces an equivalent Config. If that holds, the wizard can write whatever the user picked and trust the rest of the codebase to read it back the same way. """ from __future__ import annotations from pathlib import Path from openrun.config import ( BanisterParams, Config, HRZones, UserProfile, load_config, write_config, ) def _sample_config(tmp_path: Path) -> Config: return Config( user=UserProfile( name="Test Runner", height_cm=180.0, weight_kg=72.5, hr_max=200, lthr=178, resting_hr=48, zones=HRZones( z1=(100, 120), z2=(121, 140), z3=(141, 160), z4=(161, 180), z5=(181, 200), ), ), db_path=tmp_path / "data" / "garmin.db", banister=BanisterParams(ctl_tau_days=42.0, atl_tau_days=7.0, race_day_tl_per_km=7.0), races=(("wk 4 — 30K", "2026-06-13"), ("wk 17 — 50 mile", "2026-09-12")), ) def test_write_then_load_roundtrip(tmp_path: Path) -> None: cfg = _sample_config(tmp_path) out = tmp_path / "openrun.toml" write_config(cfg, out) loaded = load_config(out) assert loaded.user.name == cfg.user.name assert loaded.user.hr_max == cfg.user.hr_max assert loaded.user.lthr == cfg.user.lthr assert loaded.user.resting_hr == cfg.user.resting_hr assert loaded.user.weight_kg == cfg.user.weight_kg assert loaded.user.height_cm == cfg.user.height_cm assert loaded.user.zones == cfg.user.zones assert loaded.banister == cfg.banister assert loaded.races == cfg.races def test_write_handles_missing_optional_fields(tmp_path: Path) -> None: """Height/weight/lthr/rhr are optional — omitted entirely from output.""" cfg = Config( user=UserProfile(name="Minimal", hr_max=190), db_path=tmp_path / "minimal.db", ) out = tmp_path / "openrun.toml" write_config(cfg, out) body = out.read_text() assert "height_cm" not in body assert "weight_kg" not in body assert "lthr" not in body assert "resting_hr" not in body loaded = load_config(out) assert loaded.user.hr_max == 190 assert loaded.user.height_cm is None def test_write_quotes_strings_with_special_chars(tmp_path: Path) -> None: """Race labels and athlete names commonly contain Unicode em-dashes and quotes; the serialiser must escape these so the file parses back.""" cfg = Config( user=UserProfile(name='O\'Brien — "Speedy"', hr_max=200), db_path=tmp_path / "x.db", races=(('wk 17 — Cascade Crest "50M"', "2026-09-12"),), ) out = tmp_path / "openrun.toml" write_config(cfg, out) loaded = load_config(out) assert loaded.user.name == 'O\'Brien — "Speedy"' assert loaded.races == (('wk 17 — Cascade Crest "50M"', "2026-09-12"),) def test_write_creates_parent_dir(tmp_path: Path) -> None: cfg = Config(user=UserProfile(name="x", hr_max=200), db_path=tmp_path / "x.db") nested = tmp_path / "a" / "b" / "openrun.toml" write_config(cfg, nested) assert nested.exists() def test_no_races_omits_section(tmp_path: Path) -> None: cfg = Config(user=UserProfile(name="x", hr_max=200), db_path=tmp_path / "x.db", races=()) out = tmp_path / "openrun.toml" write_config(cfg, out) assert "[[races]]" not in out.read_text() assert load_config(out).races == ()