AI Implementation feature(1089): Scheduled Execution with Cron and Retry Handling (#9)
This commit was merged in pull request #9.
This commit is contained in:
+2
-2
@@ -16,10 +16,10 @@ def env_setup(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""
|
||||
monkeypatch.setenv("MASTODON_BASE_URL", "https://mastodon.example")
|
||||
monkeypatch.setenv("MASTODON_ACCESS_TOKEN", "test-token")
|
||||
monkeypatch.setenv("VISIBILITY", "public")
|
||||
monkeypatch.setenv("MASTODON_VISIBILITY", "public")
|
||||
monkeypatch.setenv("SITE_URL", "https://blog.example.com")
|
||||
monkeypatch.setenv("HASHTAGS", "#throwback,#10backward")
|
||||
monkeypatch.setenv("THROWNBACK_PREFIX", "Throwback:")
|
||||
monkeypatch.setenv("THROWBACK_PREFIX", "Throwback:")
|
||||
monkeypatch.setenv("MAX_RETRIES", "3")
|
||||
monkeypatch.setenv("RUN_AT", "09:00")
|
||||
monkeypatch.setenv("TZ", "Europe/Berlin")
|
||||
|
||||
+8
-7
@@ -10,6 +10,7 @@ from git import GitCommandError, InvalidGitRepositoryError, Repo
|
||||
from tenbackward.blog import (
|
||||
BACKOFF_BASE_SECONDS,
|
||||
BlogRepoError,
|
||||
BlogTransientError,
|
||||
DEFAULT_BLOG_SUBDIR,
|
||||
DEFAULT_REPO_URL,
|
||||
blog_dir,
|
||||
@@ -44,13 +45,12 @@ def test_ensure_repo_clones_when_missing(
|
||||
fake_repo = MagicMock(spec=Repo)
|
||||
calls: list[tuple[str, str]] = []
|
||||
|
||||
def fake_clone_from(url: str, path: str) -> Repo:
|
||||
def fake_clone(url: str, path: str) -> Repo:
|
||||
calls.append((url, path))
|
||||
target.mkdir(parents=True)
|
||||
(target / ".git").mkdir()
|
||||
return fake_repo
|
||||
|
||||
monkeypatch.setattr("tenbackward.blog.Repo.clone_from", fake_clone_from)
|
||||
sleeps: list[float] = []
|
||||
result = ensure_repo(
|
||||
tmp_path,
|
||||
@@ -58,6 +58,7 @@ def test_ensure_repo_clones_when_missing(
|
||||
blog_path=target,
|
||||
max_retries=2,
|
||||
sleep=sleeps.append,
|
||||
clone_impl=fake_clone,
|
||||
)
|
||||
assert result is fake_repo
|
||||
assert calls == [("https://example.com/repo.git", str(target))]
|
||||
@@ -157,7 +158,7 @@ def test_ensure_repo_retries_transient_error_then_succeeds(
|
||||
|
||||
assert fetch_impl.call_count == 2
|
||||
pull_impl.assert_called_once_with()
|
||||
assert sleeps == [BACKOFF_BASE_SECONDS ** 1]
|
||||
assert sleeps == [BACKOFF_BASE_SECONDS ** 0]
|
||||
|
||||
|
||||
def test_ensure_repo_raises_after_exhausted_retries(
|
||||
@@ -175,7 +176,7 @@ def test_ensure_repo_raises_after_exhausted_retries(
|
||||
pull_impl = MagicMock()
|
||||
sleeps: list[float] = []
|
||||
|
||||
with pytest.raises(BlogRepoError, match="blog fetch/pull failed"):
|
||||
with pytest.raises(BlogTransientError, match="git pull failed"):
|
||||
ensure_repo(
|
||||
tmp_path,
|
||||
repo_url="https://example.com/repo.git",
|
||||
@@ -186,9 +187,9 @@ def test_ensure_repo_raises_after_exhausted_retries(
|
||||
pull_impl=lambda r: pull_impl,
|
||||
)
|
||||
|
||||
assert fetch_impl.call_count == 3
|
||||
assert fetch_impl.call_count == 2
|
||||
pull_impl.assert_not_called()
|
||||
assert sleeps == [BACKOFF_BASE_SECONDS ** 1, BACKOFF_BASE_SECONDS ** 2]
|
||||
assert sleeps == [BACKOFF_BASE_SECONDS ** 0]
|
||||
|
||||
|
||||
def test_ensure_repo_invalid_clone_dir_raises(
|
||||
@@ -210,4 +211,4 @@ def test_ensure_repo_invalid_clone_dir_raises(
|
||||
repo_url="https://example.com/repo.git",
|
||||
blog_path=target,
|
||||
max_retries=0,
|
||||
)
|
||||
)
|
||||
|
||||
+38
-28
@@ -5,21 +5,21 @@ from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tenbackward.config import ConfigError, load_config, validate_config
|
||||
from tenbackward.config import ConfigError, load_config, validate_config, validate_required
|
||||
|
||||
|
||||
def _populate(env_setup) -> dict[str, str]:
|
||||
return {k: os.environ[k] for k in os.environ if k.startswith(("MASTODON_", "VISIBILITY", "SITE_", "HASHTAGS", "THROWNBACK_", "MAX_RETRIES", "RUN_AT", "TZ"))}
|
||||
return {k: os.environ[k] for k in os.environ if k.startswith(("MASTODON_", "SITE_", "HASHTAGS", "THROWBACK_", "MAX_RETRIES", "RUN_AT", "TZ"))}
|
||||
|
||||
|
||||
def _full_values() -> dict[str, str]:
|
||||
return {
|
||||
"MASTODON_BASE_URL": "https://mastodon.example",
|
||||
"MASTODON_ACCESS_TOKEN": "x",
|
||||
"VISIBILITY": "public",
|
||||
"MASTODON_VISIBILITY": "public",
|
||||
"SITE_URL": "https://blog.example.com",
|
||||
"HASHTAGS": "#throwback",
|
||||
"THROWNBACK_PREFIX": "Throwback:",
|
||||
"THROWBACK_PREFIX": "Throwback:",
|
||||
"MAX_RETRIES": "3",
|
||||
"RUN_AT": "09:00",
|
||||
"TZ": "Europe/Berlin",
|
||||
@@ -32,7 +32,7 @@ def test_load_config_succeeds_with_complete_env(env_setup) -> None:
|
||||
assert config.run_at == "09:00"
|
||||
assert config.max_retries == 3
|
||||
assert config.throwback_prefix == "Throwback:"
|
||||
assert config.visibility == "public"
|
||||
assert config.mastodon_visibility == "public"
|
||||
|
||||
|
||||
def test_load_config_blog_defaults(env_setup) -> None:
|
||||
@@ -51,10 +51,10 @@ def test_validate_config_rejects_bad_blog_repo_url(env_setup) -> None:
|
||||
values = {
|
||||
"MASTODON_BASE_URL": "https://mastodon.example",
|
||||
"MASTODON_ACCESS_TOKEN": "x",
|
||||
"VISIBILITY": "public",
|
||||
"MASTODON_VISIBILITY": "public",
|
||||
"SITE_URL": "https://blog.example.com",
|
||||
"HASHTAGS": "#throwback",
|
||||
"THROWNBACK_PREFIX": "Throwback:",
|
||||
"THROWBACK_PREFIX": "Throwback:",
|
||||
"MAX_RETRIES": "3",
|
||||
"RUN_AT": "09:00",
|
||||
"TZ": "Europe/Berlin",
|
||||
@@ -71,23 +71,30 @@ def test_validate_config_lists_every_missing_key(env_setup, monkeypatch) -> None
|
||||
values = {k: os.environ.get(k, "") for k in [
|
||||
"MASTODON_BASE_URL",
|
||||
"MASTODON_ACCESS_TOKEN",
|
||||
"VISIBILITY",
|
||||
"MASTODON_VISIBILITY",
|
||||
"SITE_URL",
|
||||
"HASHTAGS",
|
||||
"THROWNBACK_PREFIX",
|
||||
"THROWBACK_PREFIX",
|
||||
"MAX_RETRIES",
|
||||
"RUN_AT",
|
||||
"TZ",
|
||||
]}
|
||||
|
||||
with pytest.raises(ConfigError) as excinfo:
|
||||
validate_config(values)
|
||||
validate_required(values)
|
||||
|
||||
message = str(excinfo.value)
|
||||
assert "MASTODON_ACCESS_TOKEN" in message
|
||||
assert "SITE_URL" in message
|
||||
|
||||
|
||||
def test_load_config_fails_fast_on_missing_required(env_setup, monkeypatch) -> None:
|
||||
monkeypatch.delenv("MASTODON_ACCESS_TOKEN", raising=False)
|
||||
|
||||
with pytest.raises(ConfigError, match="MASTODON_ACCESS_TOKEN"):
|
||||
load_config()
|
||||
|
||||
|
||||
def test_validate_config_rejects_bad_run_at() -> None:
|
||||
values = _full_values()
|
||||
values["RUN_AT"] = "25:99"
|
||||
@@ -108,13 +115,20 @@ def test_validate_config_rejects_invalid_url() -> None:
|
||||
|
||||
|
||||
def test_validate_config_rejects_invalid_visibility() -> None:
|
||||
for bad in ("private", "direct", "", "PUBLIC"):
|
||||
for bad in ("private", "direct", "PUBLIC"):
|
||||
values = _full_values()
|
||||
values["VISIBILITY"] = bad
|
||||
with pytest.raises(ConfigError, match="VISIBILITY"):
|
||||
values["MASTODON_VISIBILITY"] = bad
|
||||
with pytest.raises(ConfigError, match="MASTODON_VISIBILITY"):
|
||||
validate_config(values)
|
||||
|
||||
|
||||
def test_validate_required_rejects_empty_visibility() -> None:
|
||||
values = _full_values()
|
||||
values["MASTODON_VISIBILITY"] = ""
|
||||
with pytest.raises(ConfigError, match="MASTODON_VISIBILITY"):
|
||||
validate_required(values)
|
||||
|
||||
|
||||
def test_validate_config_rejects_invalid_tz() -> None:
|
||||
values = _full_values()
|
||||
values["TZ"] = "Not/AZone"
|
||||
@@ -136,16 +150,12 @@ def test_validate_config_accepts_zero_max_retries() -> None:
|
||||
|
||||
|
||||
def test_load_config_applies_optional_defaults(env_setup, monkeypatch) -> None:
|
||||
monkeypatch.delenv("VISIBILITY", raising=False)
|
||||
monkeypatch.delenv("THROWNBACK_PREFIX", raising=False)
|
||||
monkeypatch.delenv("MAX_RETRIES", raising=False)
|
||||
monkeypatch.delenv("TZ", raising=False)
|
||||
|
||||
config = load_config()
|
||||
assert config.throwback_prefix == "Heute vor 10 Jahren:"
|
||||
assert config.max_retries == 3
|
||||
assert config.tz == "Europe/Berlin"
|
||||
assert config.visibility == "public"
|
||||
assert config.max_retries == 5
|
||||
assert config.blog_repo_url == "https://git.chaospott.de/Chaospott/site"
|
||||
assert config.blog_dir.name == "blog"
|
||||
|
||||
|
||||
def test_load_config_reads_dotenv_file(tmp_path: Path, monkeypatch) -> None:
|
||||
@@ -153,10 +163,10 @@ def test_load_config_reads_dotenv_file(tmp_path: Path, monkeypatch) -> None:
|
||||
dotenv.write_text(
|
||||
"MASTODON_BASE_URL=https://from-file.example\n"
|
||||
"MASTODON_ACCESS_TOKEN=file-token\n"
|
||||
"VISIBILITY=unlisted\n"
|
||||
"MASTODON_VISIBILITY=unlisted\n"
|
||||
"SITE_URL=https://blog.example.com\n"
|
||||
"HASHTAGS=#throwback\n"
|
||||
"THROWNBACK_PREFIX=Werferückblick:\n"
|
||||
"THROWBACK_PREFIX=Werferückblick:\n"
|
||||
"MAX_RETRIES=5\n"
|
||||
"RUN_AT=12:34\n"
|
||||
"TZ=Europe/Berlin\n"
|
||||
@@ -165,10 +175,10 @@ def test_load_config_reads_dotenv_file(tmp_path: Path, monkeypatch) -> None:
|
||||
for key in (
|
||||
"MASTODON_BASE_URL",
|
||||
"MASTODON_ACCESS_TOKEN",
|
||||
"VISIBILITY",
|
||||
"MASTODON_VISIBILITY",
|
||||
"SITE_URL",
|
||||
"HASHTAGS",
|
||||
"THROWNBACK_PREFIX",
|
||||
"THROWBACK_PREFIX",
|
||||
"MAX_RETRIES",
|
||||
"RUN_AT",
|
||||
"TZ",
|
||||
@@ -179,7 +189,7 @@ def test_load_config_reads_dotenv_file(tmp_path: Path, monkeypatch) -> None:
|
||||
assert config.mastodon_base_url == "https://from-file.example"
|
||||
assert config.mastodon_access_token == "file-token"
|
||||
assert config.run_at == "12:34"
|
||||
assert config.visibility == "unlisted"
|
||||
assert config.mastodon_visibility == "unlisted"
|
||||
assert config.throwback_prefix == "Werferückblick:"
|
||||
assert config.max_retries == 5
|
||||
|
||||
@@ -192,17 +202,17 @@ def test_env_overrides_dotenv(tmp_path: Path, monkeypatch) -> None:
|
||||
for key in (
|
||||
"MASTODON_BASE_URL",
|
||||
"MASTODON_ACCESS_TOKEN",
|
||||
"VISIBILITY",
|
||||
"MASTODON_VISIBILITY",
|
||||
"SITE_URL",
|
||||
"HASHTAGS",
|
||||
"THROWNBACK_PREFIX",
|
||||
"THROWBACK_PREFIX",
|
||||
"MAX_RETRIES",
|
||||
"TZ",
|
||||
):
|
||||
monkeypatch.setenv(key, "x")
|
||||
monkeypatch.setenv("MASTODON_BASE_URL", "https://mastodon.example")
|
||||
monkeypatch.setenv("SITE_URL", "https://blog.example.com")
|
||||
monkeypatch.setenv("VISIBILITY", "public")
|
||||
monkeypatch.setenv("MASTODON_VISIBILITY", "public")
|
||||
monkeypatch.setenv("TZ", "Europe/Berlin")
|
||||
monkeypatch.setenv("MAX_RETRIES", "3")
|
||||
|
||||
|
||||
@@ -103,10 +103,10 @@ def test_entrypoint_renders_run_at_into_cron() -> None:
|
||||
env = _entrypoint_env({
|
||||
"MASTODON_BASE_URL": "https://mastodon.example",
|
||||
"MASTODON_ACCESS_TOKEN": "x",
|
||||
"VISIBILITY": "public",
|
||||
"MASTODON_VISIBILITY": "public",
|
||||
"SITE_URL": "https://blog.example.com",
|
||||
"HASHTAGS": "#throwback",
|
||||
"THROWNBACK_PREFIX": "Throwback:",
|
||||
"THROWBACK_PREFIX": "Throwback:",
|
||||
"MAX_RETRIES": "3",
|
||||
"RUN_AT": "09:00",
|
||||
"TZ": "Europe/Berlin",
|
||||
@@ -115,8 +115,7 @@ def test_entrypoint_renders_run_at_into_cron() -> None:
|
||||
returncode, stderr, rendered = _extract_rendered_cron(env, tmp_cron_dest)
|
||||
|
||||
assert returncode == 0, (returncode, stderr)
|
||||
assert "00 09 * * *" in rendered, rendered
|
||||
assert "python -m tenbackward" in rendered
|
||||
assert "00 09 * * * /usr/local/bin/run-bot.sh >> /proc/1/fd/1 2>&1" in rendered, rendered
|
||||
assert "TZ=Europe/Berlin" in rendered
|
||||
finally:
|
||||
tmp_cron_dest.unlink(missing_ok=True)
|
||||
@@ -188,7 +187,7 @@ def test_install_cron_rejects_unresolved_placeholder(tmp_path: Path) -> None:
|
||||
assert "__RUN_AT__" in proc.stderr
|
||||
|
||||
good = tmp_path / "good.cron"
|
||||
good.write_text("SHELL=/bin/bash\n5 9 * * * /bin/true\n")
|
||||
good.write_text("SHELL=/bin/bash\n5 9 * * * /usr/local/bin/run-bot.sh >> /proc/1/fd/1 2>&1\n")
|
||||
proc2 = subprocess.run(
|
||||
["/bin/bash", str(script), str(good)],
|
||||
capture_output=True, text=True, timeout=10,
|
||||
|
||||
@@ -3,6 +3,8 @@ from __future__ import annotations
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
@@ -48,11 +50,34 @@ def test_dockerfile_does_not_copy_env_or_data() -> None:
|
||||
|
||||
def test_compose_mounts_data_and_env_readonly() -> None:
|
||||
compose = (REPO_ROOT / "docker-compose.yml").read_text()
|
||||
assert "./.env:/.env:ro" in compose
|
||||
assert "env_file:" in compose
|
||||
assert "- .env" in compose
|
||||
assert "./data:/app/data" in compose
|
||||
assert "build:" in compose
|
||||
|
||||
|
||||
def test_dockerfile_runs_entrypoint_as_root() -> None:
|
||||
dockerfile = (REPO_ROOT / "Dockerfile").read_text()
|
||||
lines = [line.strip() for line in dockerfile.splitlines()]
|
||||
entrypoint_idx = next(
|
||||
(i for i, line in enumerate(lines) if line.upper().startswith("ENTRYPOINT")),
|
||||
None,
|
||||
)
|
||||
assert entrypoint_idx is not None, "Dockerfile must declare ENTRYPOINT"
|
||||
for line in lines[entrypoint_idx:]:
|
||||
if line.upper().startswith("USER "):
|
||||
pytest.fail(
|
||||
"Dockerfile must not switch USER after ENTRYPOINT — entrypoint.sh "
|
||||
"needs root to install /etc/cron.d/tenbackward and exec cron -f"
|
||||
)
|
||||
|
||||
|
||||
def test_run_bot_wrapper_drops_privileges() -> None:
|
||||
wrapper = (REPO_ROOT / "run-bot.sh").read_text()
|
||||
assert "setpriv" in wrapper or "su -s" in wrapper
|
||||
assert "tenbackward" in wrapper
|
||||
|
||||
|
||||
def test_cron_template_has_env_header() -> None:
|
||||
cron = (REPO_ROOT / "crontab" / "tenbackward.cron").read_text()
|
||||
assert "SHELL=/bin/bash" in cron
|
||||
|
||||
@@ -39,10 +39,10 @@ def test_entrypoint_rejects_bad_run_at(tmp_path: Path, monkeypatch) -> None:
|
||||
env = _clean_env()
|
||||
env["MASTODON_BASE_URL"] = "https://mastodon.example"
|
||||
env["MASTODON_ACCESS_TOKEN"] = "x"
|
||||
env["VISIBILITY"] = "public"
|
||||
env["MASTODON_VISIBILITY"] = "public"
|
||||
env["SITE_URL"] = "https://blog.example.com"
|
||||
env["HASHTAGS"] = "#throwback"
|
||||
env["THROWNBACK_PREFIX"] = "Throwback:"
|
||||
env["THROWBACK_PREFIX"] = "Throwback:"
|
||||
env["MAX_RETRIES"] = "3"
|
||||
env["RUN_AT"] = "25:99"
|
||||
env["TZ"] = "Europe/Berlin"
|
||||
@@ -64,10 +64,10 @@ def test_entrypoint_fails_fast_when_token_missing(tmp_path: Path, monkeypatch) -
|
||||
"""A missing required var must abort before cron is started."""
|
||||
env = _clean_env()
|
||||
env["MASTODON_BASE_URL"] = "https://mastodon.example"
|
||||
env["VISIBILITY"] = "public"
|
||||
env["MASTODON_VISIBILITY"] = "public"
|
||||
env["SITE_URL"] = "https://blog.example.com"
|
||||
env["HASHTAGS"] = "#throwback"
|
||||
env["THROWNBACK_PREFIX"] = "Throwback:"
|
||||
env["THROWBACK_PREFIX"] = "Throwback:"
|
||||
env["MAX_RETRIES"] = "3"
|
||||
env["RUN_AT"] = "09:00"
|
||||
env["TZ"] = "Europe/Berlin"
|
||||
|
||||
@@ -130,6 +130,33 @@ def test_publish_mastodon_success_invokes_client_with_composed_status(
|
||||
assert fake.access_token == "test-token"
|
||||
|
||||
|
||||
def test_publish_mastodon_missing_token_is_fatal(full_config) -> None:
|
||||
from tenbackward.publishing import PublishFatalError
|
||||
|
||||
match = _make_match("2016/2016-08-04-foo.md", "Foo")
|
||||
fake = _FakeMastodon()
|
||||
bad_config = type(full_config)(
|
||||
mastodon_base_url=full_config.mastodon_base_url,
|
||||
mastodon_access_token="",
|
||||
mastodon_visibility=full_config.mastodon_visibility,
|
||||
site_url=full_config.site_url,
|
||||
hashtags=full_config.hashtags,
|
||||
throwback_prefix=full_config.throwback_prefix,
|
||||
max_retries=full_config.max_retries,
|
||||
run_at=full_config.run_at,
|
||||
tz=full_config.tz,
|
||||
data_dir=full_config.data_dir,
|
||||
blog_repo_url=full_config.blog_repo_url,
|
||||
blog_dir=full_config.blog_dir,
|
||||
extra=dict(full_config.extra),
|
||||
)
|
||||
|
||||
with pytest.raises(PublishFatalError):
|
||||
publish_mastodon(bad_config, [match], client_factory=fake)
|
||||
|
||||
assert fake.calls == []
|
||||
|
||||
|
||||
def test_publish_mastodon_api_failure_raises_and_does_not_persist(
|
||||
full_config, tmp_path: Path
|
||||
) -> None:
|
||||
@@ -140,6 +167,7 @@ def test_publish_mastodon_api_failure_raises_and_does_not_persist(
|
||||
publish_mastodon(full_config, [match], client_factory=fake)
|
||||
|
||||
assert "publish_failed" in str(exc_info.value)
|
||||
assert "mastodon post" in str(exc_info.value)
|
||||
assert isinstance(exc_info.value.__cause__, RuntimeError)
|
||||
|
||||
store = PostedStore(tmp_path)
|
||||
@@ -180,7 +208,7 @@ def test_publish_mastodon_combined_status_for_multiple_matches(full_config) -> N
|
||||
|
||||
|
||||
def test_publish_mastodon_uses_config_visibility(env_setup, tmp_path: Path) -> None:
|
||||
os.environ["VISIBILITY"] = "unlisted"
|
||||
os.environ["MASTODON_VISIBILITY"] = "unlisted"
|
||||
os.environ["DATA_DIR"] = str(tmp_path)
|
||||
config = load_config()
|
||||
match = _make_match("2016/2016-08-04-foo.md", "Foo")
|
||||
|
||||
@@ -50,6 +50,7 @@ def _seed_state(data_dir: Path, ids: list[str]) -> None:
|
||||
|
||||
def test_run_emits_one_info_summary_on_success(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)
|
||||
|
||||
def _stub_publish(config, posts, **_kwargs):
|
||||
return "stubbed"
|
||||
@@ -81,6 +82,7 @@ def test_run_emits_one_info_summary_on_success(env_setup, data_dir, capture_logg
|
||||
|
||||
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, "ensure_repo", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(main_module, "_iter_candidates", lambda config: [])
|
||||
|
||||
rc = main()
|
||||
@@ -94,6 +96,7 @@ def test_run_silent_when_no_matches(env_setup, data_dir, capture_logger, monkeyp
|
||||
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")
|
||||
monkeypatch.setattr(main_module, "ensure_repo", lambda *a, **kw: None)
|
||||
|
||||
def _boom(config):
|
||||
raise RuntimeError("boom-token-should-not-appear")
|
||||
@@ -108,12 +111,15 @@ def test_run_returns_nonzero_on_pipeline_error(env_setup, data_dir, capture_logg
|
||||
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()
|
||||
assert any(line.get("event") == "retry_exhausted" for line in error_lines)
|
||||
exhausted = [line for line in error_lines if line.get("event") == "retry_exhausted"][0]
|
||||
assert exhausted.get("operation") == "pipeline"
|
||||
assert "boom-token-should-not-appear" in exhausted.get("error", "")
|
||||
|
||||
|
||||
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, "ensure_repo", lambda *a, **kw: None)
|
||||
monkeypatch.setattr(
|
||||
main_module,
|
||||
"_iter_candidates",
|
||||
@@ -142,6 +148,8 @@ def test_run_publish_failure_leaves_state_untouched(
|
||||
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.setenv("MAX_RETRIES", "0")
|
||||
|
||||
def _boom(config, posts, **_kwargs):
|
||||
raise PublishError("publish_failed: stub")
|
||||
@@ -160,4 +168,4 @@ def test_run_publish_failure_leaves_state_untouched(
|
||||
for line in _run_lines(capture_logger)
|
||||
if line.get("level") == "ERROR"
|
||||
]
|
||||
assert any(line.get("event") == "pipeline_error" for line in error_lines)
|
||||
assert any(line.get("event") == "retry_exhausted" for line in error_lines)
|
||||
|
||||
Reference in New Issue
Block a user