From f4f71461ee8c660bd43b7a04187f443b7a173343 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 11 Sep 2026 00:05:26 +0200 Subject: [PATCH] fix: wire dry-run source state policy --- pronote_sync/pipeline/run.py | 9 +++- tests/integration/test_pipeline_runner.py | 66 ++++++++++++++++++++++- 2 files changed, 72 insertions(+), 3 deletions(-) diff --git a/pronote_sync/pipeline/run.py b/pronote_sync/pipeline/run.py index 86bba04..4c873e0 100644 --- a/pronote_sync/pipeline/run.py +++ b/pronote_sync/pipeline/run.py @@ -126,6 +126,7 @@ class PipelineRunner: :rtype: PipelineRunner """ effective_dry_run = settings.app.dry_run if dry_run is None else dry_run + persistence_enabled = not effective_dry_run theoretical_provider = get_theoretical_provider( settings.app.theoretical_agenda_path, settings.app.school_holidays_path, @@ -136,7 +137,9 @@ class PipelineRunner: AgendaComparator(theoretical_provider) if theoretical_provider is not None else None ) blog_client = BlogRSSClient(settings.blog.rss_url) if settings.blog.enabled else None - blog_state = BlogRSSState() if settings.blog.enabled else None + blog_state = ( + BlogRSSState(persistence_enabled=persistence_enabled) if settings.blog.enabled else None + ) return cls( settings=settings, pronote_fetcher=PronoteFetcher( @@ -144,7 +147,9 @@ class PipelineRunner: PronoteClient( settings.pronote, auth_state=( - PronoteAuthState() if settings.pronote.auth_mode == "qr_token" else None + PronoteAuthState(persistence_enabled=persistence_enabled) + if settings.pronote.auth_mode == "qr_token" + else None ), ), ), diff --git a/tests/integration/test_pipeline_runner.py b/tests/integration/test_pipeline_runner.py index 85d77d2..1eb830b 100644 --- a/tests/integration/test_pipeline_runner.py +++ b/tests/integration/test_pipeline_runner.py @@ -8,7 +8,13 @@ from typing import Any, cast import pytest from pydantic import SecretStr -from pronote_sync.config.settings import AISettings, AppSettings, PronoteSettings, Settings +from pronote_sync.config.settings import ( + AISettings, + AppSettings, + BlogSettings, + PronoteSettings, + Settings, +) from pronote_sync.errors import PipelineCriticalError, PipelineWarning, PronoteAuthRotationError from pronote_sync.models.agenda import Lesson, LessonStatus, SchoolEvent from pronote_sync.models.blog import BlogArticle @@ -449,6 +455,64 @@ def test_from_settings_qr_token_mode_passes_auth_state_instance( assert isinstance(constructed[0][1], PronoteAuthState) +@pytest.mark.parametrize("dry_run", [False, True]) +def test_from_settings_configures_source_state_persistence_for_dry_run( + monkeypatch: pytest.MonkeyPatch, + dry_run: bool, +) -> None: + """Composition disables source-state persistence only in dry-run mode.""" + import pronote_sync.pipeline.run as run_module + + blog_persistence: list[bool] = [] + auth_persistence: list[bool] = [] + + class RecordingBlogState: + """Blog state factory recording its persistence configuration.""" + + def __init__(self, *, persistence_enabled: bool = True) -> None: + """Record the requested persistence setting. + + :param persistence_enabled: Whether disk writes are enabled. + """ + blog_persistence.append(persistence_enabled) + + class RecordingAuthState: + """Authentication state factory recording its persistence configuration.""" + + def __init__(self, *, persistence_enabled: bool = True) -> None: + """Record the requested persistence setting. + + :param persistence_enabled: Whether disk writes are enabled. + """ + auth_persistence.append(persistence_enabled) + + class RecordingClient: + """Pronote client constructor accepting the injected auth state.""" + + def __init__(self, settings: PronoteSettings, *, auth_state: object) -> None: + """Accept the composition-root dependencies. + + :param settings: Pronote settings. + :param auth_state: Injected authentication state. + """ + del settings, auth_state + + monkeypatch.setattr(run_module, "BlogRSSState", RecordingBlogState) + monkeypatch.setattr(run_module, "PronoteAuthState", RecordingAuthState) + monkeypatch.setattr(run_module, "PronoteClient", RecordingClient) + + PipelineRunner.from_settings( + Settings( + app=AppSettings(dry_run=dry_run), + blog=BlogSettings(enabled=True), + pronote=PronoteSettings(auth_mode="qr_token"), + ) + ) + + assert blog_persistence == [not dry_run] + assert auth_persistence == [not dry_run] + + def test_runner_reuses_ical_download_and_parse_within_one_run( monkeypatch: pytest.MonkeyPatch, pipeline_inputs: tuple[Lesson, Homework],