from __future__ import annotations import io import json import logging import os from pathlib import Path import pytest from datetime import date from tenbackward import main as main_module from tenbackward.logging_setup import JsonFormatter from tenbackward.main import main from tenbackward.matching import MatchedPost from tenbackward.publishing import PublishError def _match(path: str) -> MatchedPost: return MatchedPost( path=path, title=path, date=date(2016, 8, 4), url=f"https://blog.example.com/2016/08/04/{path.split('-', 3)[-1].removesuffix('.md')}/", ) @pytest.fixture() def capture_logger() -> io.StringIO: buf = io.StringIO() handler = logging.StreamHandler(buf) handler.setFormatter(JsonFormatter()) target = logging.getLogger("tenbackward") target.handlers = [handler] target.setLevel(logging.INFO) target.propagate = False return buf def _run_lines(buf: io.StringIO) -> list[dict]: return [json.loads(line) for line in buf.getvalue().splitlines() if line.strip()] def _seed_state(data_dir: Path, ids: list[str]) -> None: from tenbackward.state import mark_posted_many mark_posted_many(list(ids), data_dir) def test_run_emits_one_info_summary_on_success(env_setup, data_dir, capture_logger, monkeypatch) -> None: monkeypatch.setenv("DATA_DIR", str(data_dir)) def _stub_publish(config, posts, **_kwargs): return "stubbed" monkeypatch.setattr(main_module, "_iter_candidates", lambda config: [_match("new-1"), _match("already-1")]) monkeypatch.setattr(main_module, "publish_mastodon", _stub_publish) _seed_state(data_dir, ["already-1"]) rc = main() assert rc == 0 lines = _run_lines(capture_logger) summary = [line for line in lines if line.get("event") == "run_complete"] assert len(summary) == 1 payload = summary[0] assert payload["scanned"] == 2 assert payload["matched"] == 2 assert payload["posted"] == 1 assert payload["skipped"] == 1 assert payload["posted_ids"] == ["new-1"] from tenbackward.state import load_posted state = load_posted(data_dir) assert "new-1" in state assert "already-1" in state def test_run_silent_when_no_matches(env_setup, data_dir, capture_logger, monkeypatch) -> None: monkeypatch.setenv("DATA_DIR", str(data_dir)) monkeypatch.setattr(main_module, "_iter_candidates", lambda config: []) rc = main() assert rc == 0 lines = _run_lines(capture_logger) summary = [line for line in lines if line.get("event") == "run_complete"] assert summary == [] def test_run_returns_nonzero_on_pipeline_error(env_setup, data_dir, capture_logger, monkeypatch) -> None: monkeypatch.setenv("DATA_DIR", str(data_dir)) monkeypatch.setenv("MAX_RETRIES", "1") def _boom(config): raise RuntimeError("boom-token-should-not-appear") monkeypatch.setattr(main_module, "_iter_candidates", _boom) rc = main() assert rc == 1 lines = _run_lines(capture_logger) summary = [line for line in lines if line.get("event") == "run_complete"] assert summary == [] error_lines = [line for line in lines if line.get("level") == "ERROR"] assert any("exc_type" in line for line in error_lines) assert "boom-token-should-not-appear" not in capture_logger.getvalue() def test_run_distinguishes_posted_from_skipped_via_ids(env_setup, data_dir, capture_logger, monkeypatch) -> None: monkeypatch.setenv("DATA_DIR", str(data_dir)) monkeypatch.setattr( main_module, "_iter_candidates", lambda config: [_match("alpha"), _match("beta"), _match("gamma")], ) monkeypatch.setattr( main_module, "publish_mastodon", lambda config, posts, **_kwargs: "stubbed", ) _seed_state(data_dir, ["beta"]) rc = main() assert rc == 0 summary = [line for line in _run_lines(capture_logger) if line.get("event") == "run_complete"] assert len(summary) == 1 payload = summary[0] assert set(payload["posted_ids"]) == {"alpha", "gamma"} assert "beta" not in payload["posted_ids"] assert payload["skipped"] == 1 assert payload["posted"] == 2 def test_run_publish_failure_leaves_state_untouched( env_setup, data_dir, capture_logger, monkeypatch ) -> None: monkeypatch.setenv("DATA_DIR", str(data_dir)) def _boom(config, posts, **_kwargs): raise PublishError("publish_failed: stub") monkeypatch.setattr(main_module, "_iter_candidates", lambda config: [_match("alpha")]) monkeypatch.setattr(main_module, "publish_mastodon", _boom) rc = main() assert rc != 0 from tenbackward.state import load_posted assert load_posted(data_dir) == [] error_lines = [ line for line in _run_lines(capture_logger) if line.get("level") == "ERROR" ] assert any(line.get("event") == "pipeline_error" for line in error_lines)