"""Tests for exclusion reporting and log-level policy (Sections 7 and 22). Excluded paths and the rule that matched them are logged at DEBUG level; the INFO summary carries only the in-scope/excluded split. Commit messages never contain exclusion details (Section 7). """ from __future__ import annotations import logging from git_auto_tracker.changes.filters import ( Exclusion, exclusion_reason, filter_change_set, inclusion_reason, ) from git_auto_tracker.changes.models import ChangeRecord, ChangeSet, ChangeType from git_auto_tracker.config import FileFilters class TestReasonFunctions: def test_exclusion_reason_names_the_pattern(self) -> None: filters = FileFilters(include=["**/*"], exclude=["runtime/**", "**/*.log"]) assert exclusion_reason("runtime/cache.dat", filters) == "matched exclude 'runtime/**'" assert exclusion_reason("logs/a.log", filters) == "matched exclude '**/*.log'" assert exclusion_reason("keep.xml", filters) is None def test_inclusion_reason_when_out_of_scope(self) -> None: filters = FileFilters(include=["Stations/**"], exclude=[]) assert inclusion_reason("System/global.xml", filters) == "no include pattern matched" assert inclusion_reason("Stations/PS1.xml", filters) is None def test_empty_include_means_everything_included(self) -> None: filters = FileFilters(include=[], exclude=["**/*.tmp"]) assert inclusion_reason("anything.xml", filters) is None def test_legacy_predicates_still_work(self) -> None: filters = FileFilters(include=["**/*"], exclude=["**/*.log"]) from git_auto_tracker.changes.filters import is_excluded, is_included assert is_included("a.xml", filters) is True assert is_excluded("a.log", filters) is True assert is_excluded("a.xml", filters) is False class TestFilterChangeSet: def _change_set(self) -> ChangeSet: return ChangeSet( added=[ ChangeRecord(path="runtime/cache.dat", change_type=ChangeType.ADDED), ChangeRecord(path="Stations/PS1.xml", change_type=ChangeType.ADDED), ], modified=[ ChangeRecord(path="logs/app.log", change_type=ChangeType.MODIFIED), ChangeRecord(path="System/global.xml", change_type=ChangeType.MODIFIED), ], ) def test_exclusions_carry_reasons(self) -> None: filters = FileFilters(include=["**/*"], exclude=["runtime/**", "**/*.log"]) kept, excluded = filter_change_set(self._change_set(), filters) assert [r.path for r in kept.added] == ["Stations/PS1.xml"] assert [r.path for r in kept.modified] == ["System/global.xml"] by_path = {e.path: e.reason for e in excluded} assert by_path["runtime/cache.dat"] == "matched exclude 'runtime/**'" assert by_path["logs/app.log"] == "matched exclude '**/*.log'" def test_out_of_scope_excluded_with_include_reason(self) -> None: filters = FileFilters(include=["Stations/**"], exclude=["**/*.log"]) _, excluded = filter_change_set(self._change_set(), filters) by_path = {e.path: e.reason for e in excluded} assert by_path["System/global.xml"] == "no include pattern matched" assert by_path["runtime/cache.dat"] == "no include pattern matched" assert by_path["logs/app.log"] == "matched exclude '**/*.log'" def test_exclusion_is_dataclass(self) -> None: record = Exclusion(path="x.tmp", reason="matched exclude '**/*.tmp'") assert record.path == "x.tmp" assert "x.tmp" in record.reason or "**/*.tmp" in record.reason class TestLogLevels: """INFO summary only; per-file detail at DEBUG (application wiring).""" def test_collect_logs_split_and_debug_details( self, git_repo, tracker_config, tmp_path, caplog ) -> None: from git_auto_tracker.application import ApplicationController from git_auto_tracker.config import AppConfig from git_auto_tracker.logging.setup import setup_logging (git_repo / "junk.log").write_text("noise", encoding="utf-8") (git_repo / "real.xml").write_text("", encoding="utf-8") config = AppConfig.from_dict(tracker_config, config_dir=tmp_path) setup_logging( config.logging, config_dir=tmp_path, repository_path=config.repository_path, override_level="DEBUG", ) # The app logger does not propagate (setup_logging), so caplog's # root-attached handler never sees the records; attach it directly. app_logger = logging.getLogger("git_auto_tracker") app_logger.addHandler(caplog.handler) try: controller = ApplicationController(config) outcome = controller.collect() finally: app_logger.removeHandler(caplog.handler) assert outcome.change_count == 1 # real.xml only debug_text = caplog.text # INFO summary: the split, no per-file exclusion detail assert "Detected 2 change(s): 1 in scope, 1 excluded by filters" in debug_text # DEBUG detail: path plus the matching rule assert "Excluded: junk.log (matched exclude '**/*.log')" in debug_text # Author resolution details land at DEBUG too assert "Author resolution detail: real.xml -> source=" in debug_text # The exclusion must NOT appear in the commit message (Section 7) assert "junk.log" not in (outcome.commit_message or "")