`source` names the code that wrote a row. It cannot name the schedule that
invoked it, and two tasks may share one executor -- so a row could not answer
the question a health record mostly exists to answer: which job broke?
Concretely, on 2026-08-11 a 425 MB probe task and the 5 GB nightly backup both
ran through config_backup_executor. The probe failed and wrote
source: scheduler/config_backup_executor
status: critical
error: Backup file was not created
which is byte-for-byte what a nightly backup failure would have written. The
row was true and unattributable, and the reflex it invited -- delete the
inconvenient row -- was correctly refused. Attribution is the actual fix: the
record stays intact and starts saying who it is about.
Carried in a ContextVar rather than an argument. Executors are invoked as
execute(config, settings) and there are ten of them, several dormant -- existing
only as a string in a database row and becoming live the moment someone inserts
a task naming them. A signature change would leave those broken in a way nothing
imports, greps or tests would reveal. Injecting the name into `config` was the
other option and is worse: `config` is what a human wrote in the task
definition, and an executor is entitled to reject keys it does not recognise.
Two properties make the ContextVar safe, both verified in the deployed runtime
rather than reasoned about:
- asyncio.to_thread propagates the context, so reporting still sees the task
after T-74 moved executor bodies into worker threads. Had it not, every row
from a real executor would have quietly lost its task while unit tests kept
passing -- so there is a test that specifically goes through report_async.
- Each asyncio Task gets its own copy, so the five concurrent executions
MAX_CONCURRENT_TASKS permits cannot read each other's value. The isolation
test yields mid-execution to force interleaving; without that it would pass
even against a shared global.
A plain await does NOT get its own copy and leaks the value to the caller, which
the runtime check showed. Both real entry points go through create_task, but
task_scope resets via token rather than depending on that.
The field is omitted, not nulled, when there is no task: report() is callable
from a script, and a null would claim a task existed with no name.
Mutation-checked: removing the scope from the engine fails both isolation tests.
Co-Authored-By: Claude <noreply@anthropic.com>
Scheduler Tests
Comprehensive test suite for The Scheduler using pytest.
Test Structure
tests/
├── conftest.py # Shared fixtures and configuration
├── test_api.py # API endpoint tests
├── test_example_executor.py # Example executor tests
├── test_doc_sync_executor.py # Documentation sync executor tests
└── README.md # This file
Running Tests
Run all tests
docker exec scheduler pytest
Run with coverage report
docker exec scheduler pytest --cov=src --cov-report=term-missing
Run specific test file
docker exec scheduler pytest tests/test_api.py
Run specific test class
docker exec scheduler pytest tests/test_api.py::TestHealthEndpoint
Run specific test
docker exec scheduler pytest tests/test_api.py::TestHealthEndpoint::test_health_endpoint_returns_healthy
Run tests by marker
# Run only unit tests
docker exec scheduler pytest -m unit
# Run only API tests
docker exec scheduler pytest -m api
# Run only executor tests
docker exec scheduler pytest -m executor
# Run only fast tests (exclude slow)
docker exec scheduler pytest -m "not slow"
Run with verbose output
docker exec scheduler pytest -v
Run with detailed failure output
docker exec scheduler pytest -vv --tb=long
Stop on first failure
docker exec scheduler pytest -x
Test Markers
Tests are organized with pytest markers:
@pytest.mark.unit- Fast unit tests with no external dependencies@pytest.mark.integration- Integration tests (may use database)@pytest.mark.api- API endpoint tests@pytest.mark.executor- Task executor tests@pytest.mark.slow- Tests that take a while to run
Coverage Reports
After running tests with coverage:
- Terminal: Shows missing lines in terminal output
- HTML: Open
htmlcov/index.htmlin browser for detailed report - JSON: Machine-readable coverage data in
coverage.json
View HTML coverage report:
# From host machine
open /home/jpmschweitzer/docker-data/scheduler/htmlcov/index.html
Writing New Tests
Test File Naming
- Test files must start with
test_ - Place in
tests/directory - Use descriptive names:
test_<module_name>.py
Test Function Naming
- Test functions must start with
test_ - Use descriptive names:
test_<what_is_being_tested>
Using Fixtures
Fixtures are defined in conftest.py:
def test_something(test_settings, auth_headers):
# Use fixtures as function parameters
assert test_settings.app_name == "Test Scheduler"
Adding Markers
@pytest.mark.unit
@pytest.mark.api
def test_health_endpoint(client):
response = client.get("/health")
assert response.status_code == 200
Async Tests
@pytest.mark.asyncio
async def test_async_function():
result = await some_async_function()
assert result is not None
Mocking
from unittest.mock import patch, MagicMock
def test_with_mock():
with patch('module.function') as mock_func:
mock_func.return_value = "mocked"
result = call_function_that_uses_it()
assert result == "mocked"
Continuous Integration
These tests are designed to run in CI pipelines:
# Example GitHub Actions
- name: Run tests
run: |
docker exec scheduler pytest --cov=src --cov-report=xml
docker exec scheduler pytest --cov=src --cov-report=html
Test Database
Tests use mocked database connections by default. For integration tests that require a real database:
- Set up a test database
- Use
POSTGRES_DB=test_schedulerenvironment variable - Run migrations before tests
- Clean up after tests
Troubleshooting
Tests fail with "ModuleNotFoundError"
# Install test dependencies
docker exec scheduler /venv/bin/pip install -r requirements.txt
Tests fail with database errors
- Tests use mocked connections by default
- Check that mocks are properly set up in conftest.py
- For integration tests, ensure test database exists
Coverage not working
# Reinstall pytest-cov
docker exec scheduler /venv/bin/pip install --upgrade pytest-cov
Best Practices
- Fast by default: Unit tests should be fast (<1s each)
- Isolated: Tests should not depend on each other
- Descriptive: Test names should clearly describe what they test
- Single assertion: Test one thing per test when possible
- Use fixtures: Reuse common setup via fixtures
- Mock external deps: Don't hit real databases/APIs in unit tests
- Mark appropriately: Use markers to categorize tests
Current Coverage
Run this to see current coverage:
docker exec scheduler pytest --cov=src --cov-report=term
Target: 80%+ code coverage for critical paths