Tasks: Scheduler & Pipeline Assembly
Input: Design documents from /specs/007-scheduler/
Prerequisites: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
Tests: This is a brownfield feature — all tasks are verification tests that confirm existing behaviour matches the spec.
Organization: Tasks are grouped by user story to enable independent implementation and testing of each story.
Format: [ID] [P?] [Story] Description
- [P]: Can run in parallel (different files, no dependencies)
- [Story]: Which user story this task belongs to (e.g., US1, US2, US3)
- Include exact file paths in descriptions
Path Conventions
- Source:
src/at repository root - Unit tests:
src/tests/unit/ - Integration tests:
src/tests/integration/ - Test execution:
make test(unit, Docker),make test-integration(integration, Docker)
Phase 1: Setup (Shared Test Infrastructure)
Purpose: Create shared test fixtures and helpers for scheduler verification tests.
- [ ] T001 Create test fixtures for scheduler entry point mocking (mock time.sleep, validate_config, configure_logging, build_collection_pipeline, push_metrics, shutdown_tracing, signal.signal) in
src/tests/unit/entrypoints/cli/conftest.py - [ ] T002 [P] Create integration test conftest for due-source selection tests (ScraperSetting factory, schema setup, per-test rollback) in
src/tests/integration/test_scheduler_due_sources_conftest.py
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Establish baseline test coverage for the existing check_timeout() and validate_config() functions before expanding to user story tests.
- [ ] T003 [P] Add test for
validate_config()raising ValueError when DATABASE_URL is empty insrc/tests/unit/config/test_settings.py - [ ] T004 [P] Add test for
validate_config()passing when DATABASE_URL is set insrc/tests/unit/config/test_settings.py
Checkpoint: Basic config validation covered; ready for user story test phases
Phase 3: User Story 1 - Scheduled Pipeline Run (Priority: P1) 🎯 MVP
Goal: Verify the run-once entry point lifecycle: config validation → jitter → run context → pipeline execution → teardown.
Independent Test: Run make test and verify all US1 scenarios pass: missing DATABASE_URL raises error, jitter sleep is applied or skipped based on RUN_IMMEDIATELY, pipeline runs and exits cleanly, finally block always executes.
Verification Tests for User Story 1
- [ ] T005 [US1] Test that
main()raises ValueError when DATABASE_URL is not set insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T006 [US1] Test that
main()callstime.sleep()with a value in [0, 180] when RUN_IMMEDIATELY is not set insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T007 [US1] Test that
main()does NOT calltime.sleep()when RUN_IMMEDIATELY is set insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T008 [US1] Test that
main()generates and binds a run_id and correlation_id to structlog context insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T009 [US1] Test that
main()registers signal handlers for SIGTERM and SIGINT insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T010 [US1] Test that
main()callsbuild_collection_pipeline()and thenpipeline.run()in sequence insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T011 [US1] Test that
main()callspush_metrics()andshutdown_tracing()in the finally block even whenpipeline.run()raises an exception insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T012 [US1] Test that
main()increments the SCRAPER_RUNS OTel counter at startup insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T013 [US1] Test that
main()records wall-clock duration in SCRAPER_DURATION histogram in the finally block insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T014 [US1] Test that
main()starts an OTel span named "scraper.run" with run.id and run.correlation_id attributes insrc/tests/unit/entrypoints/cli/test_main.py
Checkpoint: US1 fully verified — entry point lifecycle matches spec
Phase 4: User Story 2 - Pipeline Assembly & Event Wiring (Priority: P2)
Goal: Verify the composition root wires all repositories, event bus, LLM services, use cases, and event handler subscriptions correctly.
Independent Test: Run make test and verify build_collection_pipeline() returns a pipeline with the correct event bus subscriptions and that it raises ValueError when no LLM providers are active.
Verification Tests for User Story 2
- [ ] T015 [P] [US2] Test that
build_collection_pipeline()raises ValueError when no active LLM providers exist insrc/tests/unit/test_composition_root.py - [ ] T016 [P] [US2] Test that
build_collection_pipeline()returns a CollectionPipeline with an InMemoryEventBus that has a subscription for ArticleScrapedEvent → ArticleScrapedHandler insrc/tests/unit/test_composition_root.py - [ ] T017 [P] [US2] Test that
build_collection_pipeline()returns a CollectionPipeline with InMemoryEventBus subscriptions for ArticleProcessedEvent → ArticleProcessedHandler insrc/tests/unit/test_composition_root.py - [ ] T018 [P] [US2] Test that
build_collection_pipeline()wires AnalysisCompletedEvent → TagNormalizationHandler insrc/tests/unit/test_composition_root.py - [ ] T019 [P] [US2] Test that
build_collection_pipeline()wires TagNormalizationCompletedEvent → AnalysisCompletedHandler insrc/tests/unit/test_composition_root.py - [ ] T020 [P] [US2] Test that
build_collection_pipeline()wires AnalysisFailedEvent, TagNormalizationFailedEvent, and TranslationFailedEvent → FailedTaskPersistenceHandler insrc/tests/unit/test_composition_root.py - [ ] T021 [P] [US2] Test that
build_collection_pipeline()wires PipelineCompletedEvent → OtelMetricsHandler and notification handler insrc/tests/unit/test_composition_root.py - [ ] T022 [US2] Test that
build_collection_pipeline()creates 10 repository instances all sharing the same session insrc/tests/unit/test_composition_root.py - [ ] T023 [US2] Test that
build_collection_pipeline()creates a ScrapeExecutor with an on_discover_failed callback insrc/tests/unit/test_composition_root.py
Checkpoint: US2 fully verified — composition root wiring matches spec
Phase 5: User Story 3 - Due Source Selection (Priority: P2)
Goal: Verify that get_active_due() correctly selects active sources whose interval has elapsed (with 30-min tolerance) and that mark_scraped() updates last_scraped_at.
Independent Test: Run make test-integration and verify due-selection scenarios: never-scraped source included, elapsed-with-tolerance included, not-yet-elapsed excluded, inactive excluded.
Verification Tests for User Story 3
- [ ] T024 [US3] Integration test: a source that has never been scraped is included in
get_active_due()results insrc/tests/integration/test_scheduler_due_sources.py - [ ] T025 [US3] Integration test: an active source last scraped 5 hours ago with 4-hour frequency is included (elapsed > interval − tolerance) in
src/tests/integration/test_scheduler_due_sources.py - [ ] T026 [US3] Integration test: an active source last scraped 3.5 hours ago with 4-hour frequency is excluded (elapsed < interval − tolerance) in
src/tests/integration/test_scheduler_due_sources.py - [ ] T027 [US3] Integration test: an inactive source is excluded from
get_active_due()regardless of last_scraped_at insrc/tests/integration/test_scheduler_due_sources.py - [ ] T028 [US3] Integration test:
mark_scraped()setslast_scraped_atto the current time and commits insrc/tests/integration/test_scheduler_due_sources.py - [ ] T029 [US3] Integration test: CollectionPipeline.run() with no due sources publishes PipelineCompletedEvent and returns 0 in
src/tests/integration/test_scheduler_due_sources.py
Checkpoint: US3 fully verified — due-source selection matches spec
Phase 6: User Story 4 - Run Observability & Teardown (Priority: P3)
Goal: Verify that each run emits correlation-bounded logs, increments OTel counters, records duration, and flushes observability even on error.
Independent Test: Run make test and verify log binding, Sentry initialization, OTel flush, and duration recording.
Verification Tests for User Story 4
- [ ] T030 [P] [US4] Test that
main()binds a unique correlation_id to structlog context that persists across all log calls within the run insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T031 [P] [US4] Test that Sentry is initialized at import time with traces_sample_rate=0.1 when SENTRY_DSN is set in
src/tests/unit/entrypoints/cli/test_main.py - [ ] T032 [P] [US4] Test that Sentry is NOT initialized when SENTRY_DSN is not set in
src/tests/unit/entrypoints/cli/test_main.py - [ ] T033 [P] [US4] Test that
push_metrics()failure does not preventshutdown_tracing()from being called insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T034 [P] [US4] Test that
shutdown_tracing()failure does not prevent process exit insrc/tests/unit/entrypoints/cli/test_main.py
Checkpoint: US4 fully verified — observability and teardown match spec
Phase 7: User Story 5 - Signal Handling & Hard Timeout (Priority: P3)
Goal: Verify that signal handlers log and set the flag but do NOT interrupt pipeline execution, and that check_timeout() exists but is never called in the runtime path.
Independent Test: Run make test and verify signal handler behavior and timeout function behavior.
Verification Tests for User Story 5
- [ ] T035 [P] [US5] Test that
signal_handlersets_shutdown_requested = Trueand logsshutdown_signal_receivedinsrc/tests/unit/entrypoints/cli/test_main.py - [ ] T036 [P] [US5] Test that
_shutdown_requestedis not checked byCollectionPipeline.run()(pipeline continues after signal) insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T037 [P] [US5] Test that
check_timeout()returns True when elapsed >= 3000 seconds insrc/tests/unit/entrypoints/cli/test_main.py(already exists — verify it still passes) - [ ] T038 [P] [US5] Test that
check_timeout()returns False when elapsed < 3000 seconds insrc/tests/unit/entrypoints/cli/test_main.py(already exists — verify it still passes) - [ ] T039 [US5] Test that
check_timeout()is NOT called anywhere inmain()execution path (verify by mocking and asserting zero calls) insrc/tests/unit/entrypoints/cli/test_main.py
Checkpoint: US5 fully verified — signal and timeout scaffolding matches spec
Phase 8: User Story 6 - Standalone Translation Pipeline (Priority: P4)
Goal: Verify that build_translation_pipeline() returns a dict with the expected keys and that translation runs independently of collection.
Independent Test: Run make test and verify build_translation_pipeline() output structure.
Verification Tests for User Story 6
- [ ] T040 [P] [US6] Test that
build_translation_pipeline()returns a dict with keys: use_case, tag_use_case, session, analyses_translation_repository, tag_translation_repository insrc/tests/unit/test_composition_root.py - [ ] T041 [US6] Test that
build_translation_pipeline()does not create an InMemoryEventBus or notification handler insrc/tests/unit/test_composition_root.py
Checkpoint: US6 fully verified — standalone translation pipeline matches spec
Phase 9: Polish & Cross-Cutting Concerns
Purpose: Cross-story verification and cleanup.
- [ ] T042 [P] Add test for
CollectionPipeline.run()that verifiesmark_scraped()is called for each due setting after discovery insrc/tests/unit/infrastructure/collection/test_collection_pipeline.py - [ ] T043 [P] Add test for
CollectionPipeline.run()that verifies pre-fetch dedup filter checks URL hashes against already-analyzed articles insrc/tests/unit/infrastructure/collection/test_collection_pipeline.py - [ ] T044 [P] Add test for
CollectionPipeline.run()that verifies post-fetch dedup removes duplicate URLs insrc/tests/unit/infrastructure/collection/test_collection_pipeline.py - [ ] T045 Add test verifying that
main()initializes the default HTTP client viainit_default_client(HttpClient.build_default())insrc/tests/unit/entrypoints/cli/test_main.py - [ ] T046 Run
make testand verify all unit tests pass - [ ] T047 Run
make test-integrationand verify all integration tests pass
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies — can start immediately
- Foundational (Phase 2): Depends on Setup completion — BLOCKS all user stories
- User Stories (Phase 3–8): All depend on Foundational phase completion
- US1 and US2 can proceed in parallel (different test files)
- US3 depends on integration test conftest from Setup (T002)
- US4 depends on US1 test infrastructure (conftest.py from T001)
- US5 depends on US1 test infrastructure (conftest.py from T001)
- US6 is independent of US1–US5 (different file: test_composition_root.py)
- Polish (Phase 9): Depends on all user stories being complete
User Story Dependencies
- US1 (P1): Depends on Phase 1 + Phase 2 — no dependencies on other stories
- US2 (P2): Depends on Phase 1 + Phase 2 — independent of US1
- US3 (P2): Depends on Phase 1 (T002) + Phase 2 — independent of US1/US2
- US4 (P3): Depends on Phase 1 (T001) + Phase 2 — reuses US1 conftest
- US5 (P3): Depends on Phase 1 (T001) + Phase 2 — reuses US1 conftest
- US6 (P4): Depends on Phase 1 + Phase 2 — independent of US1–US5
Within Each User Story
- Tests within a story marked [P] can run in parallel
- Each test is self-contained with its own mocks/fixtures
Parallel Opportunities
- T001 and T002 can run in parallel (different conftest files)
- T003 and T004 can run in parallel (same file, different test functions)
- T015–T021 can all run in parallel (independent event bus wiring tests)
- T024–T029 run sequentially (integration tests with shared DB state)
- T030–T034 can run in parallel (independent observability tests)
- T035–T039 can run in parallel (independent signal/timeout tests)
- T040–T041 can run in parallel (independent translation pipeline tests)
- T042–T044 can run in parallel (independent pipeline behaviour tests)
Parallel Example: User Story 1
# These can all be implemented in parallel (different test cases, same file):
Task T005: "Test main() raises ValueError when DATABASE_URL not set"
Task T006: "Test main() calls time.sleep() in [0, 180] when RUN_IMMEDIATELY not set"
Task T007: "Test main() does NOT call time.sleep() when RUN_IMMEDIATELY set"
Task T008: "Test main() generates run_id and correlation_id"
Task T009: "Test main() registers SIGTERM/SIGINT handlers"
Task T010: "Test main() calls build_collection_pipeline() then pipeline.run()"
Task T011: "Test main() calls push_metrics/shutdown_tracing in finally on error"
Task T012: "Test main() increments SCRAPER_RUNS counter"
Task T013: "Test main() records SCRAPER_DURATION histogram"
Task T014: "Test main() starts OTel span scraper.run"Implementation Strategy
MVP First (User Story 1 Only)
- Complete Phase 1: Setup (T001–T002)
- Complete Phase 2: Foundational (T003–T004)
- Complete Phase 3: User Story 1 (T005–T014)
- STOP and VALIDATE:
make test— all US1 tests pass - Entry point lifecycle is now verified
Incremental Delivery
- Setup + Foundational → Test infrastructure ready
- Add US1 → Verify entry point lifecycle (MVP!)
- Add US2 → Verify composition root wiring
- Add US3 → Verify due-source selection (integration)
- Add US4 → Verify observability and teardown
- Add US5 → Verify signal/timeout scaffolding
- Add US6 → Verify standalone translation pipeline
- Add Polish → Cross-cutting tests and final validation
Parallel Team Strategy
With multiple developers:
- Team completes Setup + Foundational together
- Once Foundational is done:
- Developer A: US1 (test_main.py) + US4 (test_main.py) + US5 (test_main.py)
- Developer B: US2 (test_composition_root.py) + US6 (test_composition_root.py)
- Developer C: US3 (integration tests)
- Stories complete and integrate independently
Notes
- This is a brownfield feature: tasks write verification tests, not implementation code
- All existing tests must continue to pass (no regressions)
- Integration tests (US3) require
make test-integrationwith a running PostgreSQL - Unit tests must NOT require a database (constitution principle III)
- The conftest.py fixtures must mock all side effects (sleep, signals, DB, OTel)
- The existing
inspect.getsource()tests in test_composition_root.py should be preserved; new behavioural tests are added alongside