From 5097018bd4301baf01354ef9cbeadc012a063424 Mon Sep 17 00:00:00 2001 From: Yurii214 <216080096+Yurii214@users.noreply.github.com> Date: Fri, 31 Jul 2026 17:14:59 +0000 Subject: [PATCH] fix(inbox): case-fold configured extensions to match scan() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scan() compares `path.suffix.lower()` against cfg.extensions, but load_config stored the configured list verbatim. a config like `inbox.extensions: [".MD"]` therefore matched no file at all and silently skipped the whole inbox, since the compared side is always lowercased. case-fold configured extensions in load_config so the config side matches the file side — the same defensive coercion the `enabled: "false"` handling already applies. --- src/vouch/inbox.py | 5 ++++- tests/test_inbox.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/vouch/inbox.py b/src/vouch/inbox.py index 963859a4..7f41681c 100644 --- a/src/vouch/inbox.py +++ b/src/vouch/inbox.py @@ -65,8 +65,11 @@ def load_config(store: KBStore) -> InboxConfig: return InboxConfig( enabled=coerce_bool(raw.get("enabled", True), True), min_chars=int(raw.get("min_chars", DEFAULT_MIN_CHARS)), + # lowercase to match scan()'s `path.suffix.lower()` compare — the file + # side is already case-folded, so a verbatim ".MD" here would match no + # file at all and silently skip the whole inbox. extensions=( - tuple(str(e) for e in extensions) + tuple(str(e).lower() for e in extensions) if isinstance(extensions, list) else DEFAULT_EXTENSIONS ), diff --git a/tests/test_inbox.py b/tests/test_inbox.py index 05aa1bd7..88af74c0 100644 --- a/tests/test_inbox.py +++ b/tests/test_inbox.py @@ -70,6 +70,24 @@ def test_scan_skips_short_files_and_foreign_extensions(store: KBStore) -> None: assert sorted(result.skipped) == ["binary.png", "tiny.md"] +def test_load_config_lowercases_configured_extensions(store: KBStore) -> None: + """Regression: scan() matches `path.suffix.lower()`, so a verbatim + uppercase ".MD" in config matched no file at all and silently skipped the + whole inbox. load_config must case-fold configured extensions to the file + side, the same way `enabled: "false"` coercion is handled defensively.""" + store.config_path.write_text( + store.config_path.read_text(encoding="utf-8") + + '\ninbox:\n extensions: [".MD", ".TXT"]\n', + encoding="utf-8", + ) + assert inbox.load_config(store).extensions == (".md", ".txt") + + _drop(store, "notes.md") + result = inbox.scan(store, store.root / "inbox") + assert len(result.proposed) == 1 + assert result.skipped == [] + + def test_scan_disabled_via_config_is_noop(store: KBStore) -> None: store.config_path.write_text( store.config_path.read_text(encoding="utf-8") + "\ninbox:\n enabled: false\n",