Repository navigation
[2C-04] Align EXPIRY_DEFAULTS to v0.9 Q4, add SERVER_TZ config, add Time Block duration floor - #211
Merged
Merged
Conversation
Required runtime dep for the SERVER_TZ feature: Windows hosts have no system tzdata, so zoneinfo.ZoneInfo() fails without the PyPI package. Pinning ensures deterministic TZ data across dev (Windows), CI, and the prod container.
Adds SERVER_TZ as a Pydantic Settings field (default America/Chicago, env var BRAIN3_SERVER_TZ). A field_validator resolves the value via ZoneInfo at startup so a bad IANA key fails fast instead of silently producing wrong expiries. The settings.server_tz property returns the resolved ZoneInfo for callers.
… floor and EOD-local routine_checklist (#203) Per Pass 2 Chunk 2 E3 resolution (Option A — realign shipped code to v0.9 Q4): EXPIRY_DEFAULTS: - habit_nudge: 2h → 4h - routine_checklist: 4h → EOD-local (4h kept as fallback only) - checkin_prompt: 12h → 2h - time_block_reminder: 1h → 30m (floor; further floored by block_duration) - pattern_observation: 24h → 48h - deadline_event_alert / stale_work_nudge: unchanged calculate_expires_at gains keyword-only scheduled_date and block_duration parameters: - time_block_reminder returns delivered_at + min(30m, block_duration) when block_duration is supplied. - routine_checklist returns midnight at the start of the day after scheduled_date, in settings.SERVER_TZ. Falls back to the 4h default when scheduled_date is absent. The PATCH-to-delivered handler now passes notification.scheduled_date through to calculate_expires_at so routine_checklist hits the EOD path. block_duration has no caller infrastructure yet — no TimeBlock entity exists in the repo. The parameter is wired for the future time-block scheduler; current time_block_reminder notifications get the 30m default. This is the second 'first consumer instantiates' instance (after Group 1 PR #35) — flagged for CLAUDE.md codification at Group 2 close. All existing expiry tests rewritten to v0.9 values; new test classes TestTimeBlockFloor and TestRoutineChecklistEOD cover the new branches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Realigns the seven
EXPIRY_DEFAULTSto the v0.9 §Q4 authoritative table, introduces ablock_durationfloor fortime_block_reminder(min(30m, block_duration)), and switchesroutine_checklistfrom a flat+4hto EOD-local — midnight at the start of the day after the notification'sscheduled_date, computed in a newSERVER_TZconfig setting. The PATCH-to-delivered handler now threadsscheduled_dateintocalculate_expires_atso the EOD path is exercised in production.SERVER_TZis a Pydantic Settings field validated at startup viazoneinfo.ZoneInfo— a bad IANA key fails fast rather than silently producing wrong expiries. Addedtzdatatorequirements.txtbecause Windows hosts have no system tzdata andzoneinfofalls back to the PyPI package.Closes #203
Changes
app/config.py— AddedSERVER_TZ: strfield (env varBRAIN3_SERVER_TZ, defaultAmerica/Chicago),_validate_server_tzfield validator, andserver_tzproperty returning the resolvedZoneInfo..env.example— Documented the newBRAIN3_SERVER_TZenv var alongside the existingBRAIN3_APP_BEARER_TOKEN.requirements.txt— Addedtzdata==2026.2as a runtime dep.app/services/notification_defaults.py— UpdatedEXPIRY_DEFAULTSto v0.9 §Q4 values; added keyword-onlyscheduled_dateandblock_durationparameters tocalculate_expires_at; new branches for the time-block floor and routine_checklist EOD-local logic; both fall back gracefully when their respective inputs are missing.app/routers/notification.py— PATCH-to-delivered handler now passesscheduled_date=notification.scheduled_datesoroutine_checklisthits the EOD path. (block_durationis not threaded — see Deviations.)tests/test_notification_expiry.py— Constants and per-type calculation tests rewritten to assert v0.9 values. NewTestTimeBlockFloor(5 tests) andTestRoutineChecklistEOD(4 tests) classes cover the new branches.test_habit_nudge_expires_approx_2hintegration test renamed/rebanded to_approx_4h.How to Verify
git checkout feat/2C-04-expiry-defaults && pip install -r requirements-dev.txtpytest -v tests/test_notification_expiry.py— 58 tests pass; theTestTimeBlockFloorandTestRoutineChecklistEODclasses are the new behavior.pytest -v— full suite (1371 tests) passes.ruff check .— clean.python -c \"from app.config import settings; print(settings.SERVER_TZ, settings.server_tz)\"→America/Chicago zoneinfo.ZoneInfo(key='America/Chicago').BRAIN3_SERVER_TZ=Bogus/Zone python -c \"from app.config import settings\"raisesValueError: SERVER_TZ 'Bogus/Zone' is not a valid IANA zoneinfo keyat startup.Deviations
SERVER_TZdefault isAmerica/Chicago, notAmerica/New_Yorkas the issue body suggested. L confirmed during the ticket's open question: BRAIN is self-hosted single-user out of Springfield MO, which is Central. The NY suggestion in the issue body was a Stellan oversight. Spec amendment to follow at the next reconciliation pass.-05:00CDT), not the spec's example-04:00Eastern. Same reason — the test is faithful to the intent (delivered 22:15 local on scheduled day → expires next-day 00:00 local inSERVER_TZ), adapted to the actualSERVER_TZvalue. The spec's-04example would only make sense ifSERVER_TZwere Eastern. A separate testtest_eod_local_uses_server_tz_settingmonkeypatchesSERVER_TZtoAmerica/New_Yorkand asserts the same shape, proving the calculation respects the setting.tzdatatorequirements.txt(out-of-spec but functionally required). Windows hosts have no system tzdata. Withouttzdata,ZoneInfo('America/Chicago')raisesZoneInfoNotFoundErrorandapp.config.settings = Settings()blows up at module load. Pinningtzdata==2026.2matches the rest of the file's pinning style and gives deterministic TZ data across dev / CI / container.block_durationis not threaded into the PATCH-to-delivered handler. NoTimeBlockentity exists in the repo today; there is nothing to look up at delivery time. The parameter is wired intocalculate_expires_atfor the future time-block scheduler — when that scheduler lands, its creation path is the natural place to resolve and passblock_duration. Currenttime_block_remindernotifications get the 30m default. This is the second 'first consumer instantiates' instance flagged in the Group 2 brief (Group 1 PR SEC: CORS allows all origins with no production differentiation #35 was the first). L confirmed BRAIN will roll the codification into a brain3 CLAUDE.md v3 amendment at Group 2 close — no action required from this PR beyond the flag.Test Results
Touched-file detail (58/58):
The new behavior coverage:
TestTimeBlockFloor::test_block_duration_under_floor_wins—block_duration=10m→+10m.TestTimeBlockFloor::test_block_duration_over_floor_uses_floor—block_duration=2h→+30m.TestTimeBlockFloor::test_block_duration_equal_floor—block_duration=30m→+30m.TestTimeBlockFloor::test_no_block_duration_uses_default— omitted →+30m.TestTimeBlockFloor::test_block_duration_ignored_for_other_types—habit_nudgeignores it.TestRoutineChecklistEOD::test_eod_local_late_evening— scheduled 2026-04-20, delivered 22:15 CDT → 2026-04-21T00:00 CDT (the spec example, adapted to Chicago).TestRoutineChecklistEOD::test_eod_local_early_morning— same shape, early delivery.TestRoutineChecklistEOD::test_eod_local_uses_server_tz_setting— monkeypatchesSERVER_TZto NY, asserts the calc honors it.TestRoutineChecklistEOD::test_falls_back_to_default_when_scheduled_date_missing— defensive fallback.Acceptance Checklist
tests/test_notification_expiry.pyupdated to assert v0.9 values.time_block_reminderwithblock_duration=10minyields expiry ofdelivered_at + 10min, notdelivered_at + 30min.routine_checklistscheduled for2026-04-20and delivered at22:15local expires at2026-04-21T00:00local (notdelivered_at + 4hwhich would cross into the next day). Adapted toSERVER_TZ=America/Chicagoper L.SERVER_TZsetting accessible viasettings.SERVER_TZand consumed bycalculate_expires_at.