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:
2026-08-04 20:48:16 +00:00
parent 3545b81546
commit 5f6ddf76a4
27 changed files with 644 additions and 272 deletions
+38 -28
View File
@@ -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")