diff --git a/docs/architecture/logging.md b/docs/architecture/logging.md index 03d93d9..cc82751 100644 --- a/docs/architecture/logging.md +++ b/docs/architecture/logging.md @@ -56,7 +56,7 @@ Any matching key is replaced with the string `[REDACTED]`. | `configure_json_logging(level=INFO)` | First line of `main.main()`. Idempotent — clears existing handlers. | n/a | n/a | | `log_startup(**fields)` | After config load + `data_dir.mkdir`, before the pipeline pass. | `INFO` | `startup` | | `log_error(event, *, exc=None, **fields)` | When a pipeline attempt raises, or when the retry budget is exhausted. | `ERROR` | the supplied event name; `exc_type` added when `exc` is provided. | -| `log_run_summary(scanned, matched, posted, skipped, posted_ids)` | Once per successful pass. **Silently skipped** when all counters are zero and no ids were posted. | `INFO` | `run_complete` | +| `log_run_summary(scanned, matched, posted, skipped, posted_ids)` | Once per successful pass. Always emitted (including zero-counter passes) so debug and manual runs clearly show the scan completed. | `INFO` | `run_complete` | ## Startup payload @@ -87,8 +87,9 @@ Each is added to the `startup` event payload under its own key. } ``` -A pass with no candidates emits **no line at all**, matching the -"silent on no matches" contract. +A pass with no candidates still emits a single `run_complete` +summary line with all counters set to zero, so operators can confirm +the scan ran and found nothing to publish. # Examples diff --git a/src/tenbackward/logging_setup.py b/src/tenbackward/logging_setup.py index dedfe4b..6522a45 100644 --- a/src/tenbackward/logging_setup.py +++ b/src/tenbackward/logging_setup.py @@ -113,11 +113,9 @@ def log_run_summary( ) -> None: """Emit a single structured info line on successful completion. - A run with no matching posts is silent — no log line is emitted. + Always emitted (even with zero counters) so debug and manual runs + clearly show that scanning completed and found nothing. """ - if scanned == 0 and matched == 0 and posted == 0 and skipped == 0 and not posted_ids: - return - fields = _redact_dict( { "event": "run_complete", diff --git a/src/tenbackward/matching.py b/src/tenbackward/matching.py index 3017098..44646bd 100644 --- a/src/tenbackward/matching.py +++ b/src/tenbackward/matching.py @@ -16,6 +16,8 @@ _LOGGER = logging.getLogger("tenbackward.matching") _FILENAME_PATTERN = re.compile(r"^(\d{4})-(\d{2})-(\d{2})-(.+)$") +_POST_SUFFIXES = (".md", ".markdown") + _NON_ALNUM_RE = re.compile(r"[^a-z0-9]+") _DEFAULT_TZ = "Europe/Berlin" @@ -194,7 +196,16 @@ def find_anniversary_matches( return [] matches: list[MatchedPost] = [] - for md_path in sorted(post_root.rglob("*.md")): + seen: set[Path] = set() + candidates: list[Path] = [] + for suffix in _POST_SUFFIXES: + for md_path in sorted(post_root.rglob(f"*{suffix}")): + if md_path in seen: + continue + seen.add(md_path) + candidates.append(md_path) + candidates.sort() + for md_path in candidates: if not md_path.is_file(): continue try: diff --git a/tests/test_logging_setup.py b/tests/test_logging_setup.py index 4ff68be..23f614d 100644 --- a/tests/test_logging_setup.py +++ b/tests/test_logging_setup.py @@ -61,10 +61,19 @@ def test_log_run_summary_emits_one_line_with_counters() -> None: assert payload["posted_ids"] == ["x"] -def test_log_run_summary_silent_when_no_matches() -> None: +def test_log_run_summary_always_emits_summary_line_with_zero_counters() -> None: buf = _capture(logging.getLogger("tenbackward")) log_run_summary(0, 0, 0, 0, []) - assert buf.getvalue() == "" + lines = [line for line in buf.getvalue().splitlines() if line.strip()] + assert len(lines) == 1 + payload = json.loads(lines[0]) + assert payload["message"] == "run complete" + assert payload["event"] == "run_complete" + assert payload["scanned"] == 0 + assert payload["matched"] == 0 + assert payload["posted"] == 0 + assert payload["skipped"] == 0 + assert payload["posted_ids"] == [] def test_log_startup_emits_single_info_line() -> None: diff --git a/tests/test_matching.py b/tests/test_matching.py index 84c9538..ef47308 100644 --- a/tests/test_matching.py +++ b/tests/test_matching.py @@ -222,6 +222,48 @@ def test_missing_post_root_returns_empty(tmp_path: Path) -> None: assert find_anniversary_matches(tmp_path / "does-not-exist", SITE_URL, today=today) == [] +def test_markdown_extension_is_discovered(tmp_path: Path) -> None: + root = tmp_path / "_posts" / "blog" + _write_post( + root, + "2014/2014-08-04-foo.markdown", + body="---\ntitle: Foo\ndate: 2014-08-04\n---\nbody\n", + ) + + today = date(2024, 8, 4) + matches = find_anniversary_matches(root, SITE_URL, today=today) + + assert len(matches) == 1 + match = matches[0] + assert match.path == "2014/2014-08-04-foo.markdown" + assert match.title == "Foo" + assert match.date == date(2014, 8, 4) + assert match.url == "https://chaospott.de/2014/08/04/foo/" + + +def test_markdown_and_md_are_discovered_together(tmp_path: Path) -> None: + root = tmp_path / "_posts" / "blog" + _write_post( + root, + "2014/2014-08-04-from-md.md", + body="---\ntitle: Md\ndate: 2014-08-04\n---\n", + ) + _write_post( + root, + "2014/2014-08-04-from-markdown.markdown", + body="---\ntitle: Markdown\ndate: 2014-08-04\n---\n", + ) + + today = date(2024, 8, 4) + matches = find_anniversary_matches(root, SITE_URL, today=today) + + paths = {m.path for m in matches} + assert paths == { + "2014/2014-08-04-from-md.md", + "2014/2014-08-04-from-markdown.markdown", + } + + def test_invalid_filename_is_skipped(tmp_path: Path) -> None: root = tmp_path / "_posts" / "blog" _write_post(root, "2014/not-a-date.md", body="---\ntitle: Bad\n---\n") diff --git a/tests/test_run_logging.py b/tests/test_run_logging.py index 0171af2..b354636 100644 --- a/tests/test_run_logging.py +++ b/tests/test_run_logging.py @@ -80,7 +80,7 @@ def test_run_emits_one_info_summary_on_success(env_setup, data_dir, capture_logg assert "already-1" in state -def test_run_silent_when_no_matches(env_setup, data_dir, capture_logger, monkeypatch) -> None: +def test_run_emits_zero_summary_when_no_matches(env_setup, data_dir, capture_logger, monkeypatch) -> None: monkeypatch.setenv("DATA_DIR", str(data_dir)) monkeypatch.setattr(main_module, "ensure_repo", lambda *a, **kw: None) monkeypatch.setattr(main_module, "_iter_candidates", lambda config: []) @@ -90,7 +90,13 @@ def test_run_silent_when_no_matches(env_setup, data_dir, capture_logger, monkeyp lines = _run_lines(capture_logger) summary = [line for line in lines if line.get("event") == "run_complete"] - assert summary == [] + assert len(summary) == 1 + payload = summary[0] + assert payload["scanned"] == 0 + assert payload["matched"] == 0 + assert payload["posted"] == 0 + assert payload["skipped"] == 0 + assert payload["posted_ids"] == [] def test_run_returns_nonzero_on_pipeline_error(env_setup, data_dir, capture_logger, monkeypatch) -> None: