"""Configuration layering: CLI flag > frontmatter > default.""" from __future__ import annotations import pytest from wapdown.config import DEFAULT_MAX_CARD_BYTES, ConfigError, DeckConfig class TestPrecedence: def test_defaults_when_nothing_is_given(self): config = DeckConfig.resolve() assert config.split_level == 0 assert config.max_card_bytes == DEFAULT_MAX_CARD_BYTES assert config.menu is True assert config.menu_style == "links" assert config.title is None def test_frontmatter_beats_default(self): config = DeckConfig.resolve({"split_level": "2", "title": "Trail"}) assert config.split_level == 2 assert config.title == "Trail" def test_cli_beats_frontmatter(self): config = DeckConfig.resolve({"split_level": "2"}, {"split_level": 3}) assert config.split_level == 3 def test_absent_cli_option_does_not_clobber_frontmatter(self): # argparse hands every unset option through as None; that must mean # "not given" rather than "set to nothing". config = DeckConfig.resolve({"title": "Trail"}, {"title": None, "menu": None}) assert config.title == "Trail" assert config.menu is True def test_unknown_frontmatter_key_warns_but_does_not_fail(self, capsys): config = DeckConfig.resolve({"hyphenate": "true", "title": "T"}) assert config.title == "T" assert "unknown frontmatter key 'hyphenate'" in capsys.readouterr().err class TestCoercion: @pytest.mark.parametrize("raw", ["true", "yes", "1", "on", "TRUE"]) def test_truthy_strings(self, raw): assert DeckConfig.resolve({"menu": raw}).menu is True @pytest.mark.parametrize("raw", ["false", "no", "0", "off", "FALSE"]) def test_falsy_strings(self, raw): assert DeckConfig.resolve({"menu": raw}).menu is False def test_menu_style_is_case_insensitive(self): assert DeckConfig.resolve({"menu_style": "SELECT"}).menu_style == "select" def test_hyphenated_frontmatter_key_is_accepted(self): assert DeckConfig.resolve({"split-level": "2"}).split_level == 2 def test_empty_title_becomes_none(self): assert DeckConfig.resolve({"title": ""}).title is None class TestValidation: @pytest.mark.parametrize("value", [-1, 7]) def test_split_level_out_of_range(self, value): with pytest.raises(ConfigError, match="split_level"): DeckConfig.resolve(overrides={"split_level": value}) def test_negative_byte_budget(self): with pytest.raises(ConfigError, match="max_card_bytes"): DeckConfig.resolve(overrides={"max_card_bytes": -1}) def test_unknown_menu_style(self): with pytest.raises(ConfigError, match="menu_style"): DeckConfig.resolve(overrides={"menu_style": "carousel"}) def test_unknown_image_policy(self): with pytest.raises(ConfigError, match="images"): DeckConfig.resolve(overrides={"images": "wbmp"}) def test_non_numeric_split_level_in_frontmatter(self): # Previously an uncaught ValueError escaping as a traceback. with pytest.raises(ConfigError, match="split_level must be a number"): DeckConfig.resolve({"split_level": "deep"}) def test_non_boolean_menu_in_frontmatter(self): with pytest.raises(ConfigError, match="menu must be a boolean"): DeckConfig.resolve({"menu": "sometimes"})