"""Unit tests for config module constants.""" import importlib from pathlib import Path from devx.config import ( CONVENTIONAL_RE, DEFAULT_PER_PAGE, DEFAULT_TIMEOUT, GITEA_API_URL, MAX_RETRIES, RETRY_BACKOFF_BASE, RETRY_STATUS_CODES, TASK_ID_RE, TASK_PREFIX, VIKUNJA_API_URL, ) class TestConfigConstants: def test_api_urls(self) -> None: assert "api/v1" in GITEA_API_URL assert "api/v1" in VIKUNJA_API_URL def test_timeouts(self) -> None: assert DEFAULT_TIMEOUT == 30 assert DEFAULT_PER_PAGE == 50 def test_task_prefix(self) -> None: assert TASK_PREFIX == "DEVX" def test_task_id_re(self) -> None: assert TASK_ID_RE.search("DEVX-1") assert TASK_ID_RE.search("DEVX-123") assert not TASK_ID_RE.search("DEVX-") assert not TASK_ID_RE.search("other text") def test_conventional_re(self) -> None: assert CONVENTIONAL_RE.match("feat: add feature") assert CONVENTIONAL_RE.match("fix(scope): bug fix") assert not CONVENTIONAL_RE.match("random message") assert not CONVENTIONAL_RE.match("feat:") assert not CONVENTIONAL_RE.match("BREAKING CHANGE: something") def test_retry_config(self) -> None: assert MAX_RETRIES == 3 assert RETRY_BACKOFF_BASE == 2 assert 429 in RETRY_STATUS_CODES assert 500 in RETRY_STATUS_CODES assert 502 in RETRY_STATUS_CODES assert 503 in RETRY_STATUS_CODES assert 504 in RETRY_STATUS_CODES class TestPyprojectReading: """Test that config.py reads [tool.devx] from pyproject.toml.""" def test_pyproject_provides_values(self) -> None: """When pyproject.toml has [tool.devx], values are read from it.""" import devx.config as cfg # devx's own pyproject.toml has task_prefix=DEVX, vikunja_project_id=8 assert cfg.TASK_PREFIX == "DEVX" assert cfg.VIKUNJA_PROJECT_ID == 8 assert cfg.REPO_OWNER == "oblachno-oss" def test_env_overrides_pyproject(self, monkeypatch: object) -> None: """Env vars take priority over pyproject.toml.""" monkeypatch.setenv("DEVX_TASK_PREFIX", "CUSTOM") import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "CUSTOM" assert cfg.TASK_ID_RE.search("CUSTOM-42") monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) importlib.reload(cfg) def test_no_pyproject_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None: """When no pyproject.toml exists, defaults are used.""" monkeypatch.chdir(tmp_path) monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False) monkeypatch.delenv("DEVX_REPO_OWNER", raising=False) import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "DEVX" assert cfg.VIKUNJA_PROJECT_ID == 6 assert cfg.REPO_OWNER == "" importlib.reload(cfg) def test_invalid_toml_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None: """When pyproject.toml is invalid TOML, defaults are used.""" (tmp_path / "pyproject.toml").write_text("invalid toml {{{") monkeypatch.chdir(tmp_path) monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "DEVX" importlib.reload(cfg) def test_no_devx_section_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None: """When pyproject.toml has no [tool.devx], defaults are used.""" (tmp_path / "pyproject.toml").write_text('[project]\nname = "test"\n') monkeypatch.chdir(tmp_path) monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False) import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "DEVX" assert cfg.VIKUNJA_PROJECT_ID == 6 importlib.reload(cfg) def test_pyproject_int_value_used(self, monkeypatch: object, tmp_path: Path) -> None: """When pyproject.toml has an int value, it is used (covers _get_int return).""" (tmp_path / "pyproject.toml").write_text('[project]\nname = "test"\n[tool.devx]\nvikunja_project_id = 42\n') monkeypatch.chdir(tmp_path) monkeypatch.delenv("DEVX_VIKUNJA_PROJECT_ID", raising=False) import devx.config as cfg importlib.reload(cfg) assert cfg.VIKUNJA_PROJECT_ID == 42 importlib.reload(cfg) def test_env_int_override(self, monkeypatch: object, tmp_path: Path) -> None: """Env var override for int config takes priority over pyproject.toml.""" (tmp_path / "pyproject.toml").write_text('[project]\nname = "test"\n[tool.devx]\nvikunja_project_id = 42\n') monkeypatch.chdir(tmp_path) monkeypatch.setenv("DEVX_VIKUNJA_PROJECT_ID", "99") import devx.config as cfg importlib.reload(cfg) assert cfg.VIKUNJA_PROJECT_ID == 99 importlib.reload(cfg) def test_tool_not_dict_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None: """When [tool] is not a dict, defaults are used.""" (tmp_path / "pyproject.toml").write_text('tool = "not a dict"\n') monkeypatch.chdir(tmp_path) monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "DEVX" importlib.reload(cfg) def test_devx_not_dict_falls_back_to_defaults(self, monkeypatch: object, tmp_path: Path) -> None: """When [tool.devx] is not a dict, defaults are used.""" (tmp_path / "pyproject.toml").write_text('[tool]\ndevx = "not a dict"\n') monkeypatch.chdir(tmp_path) monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "DEVX" importlib.reload(cfg) class TestTaskPrefixOverride: def test_task_prefix_from_env(self, monkeypatch: object) -> None: """Verify TASK_PREFIX reads from DEVX_TASK_PREFIX env var.""" monkeypatch.setenv("DEVX_TASK_PREFIX", "INFRA") import devx.config as cfg importlib.reload(cfg) assert cfg.TASK_PREFIX == "INFRA" assert cfg.TASK_ID_RE.search("INFRA-42") assert not cfg.TASK_ID_RE.search("DEVX-42") monkeypatch.delenv("DEVX_TASK_PREFIX", raising=False) importlib.reload(cfg)