Skip to content

[2C-04] Align EXPIRY_DEFAULTS to v0.9 Q4, add SERVER_TZ config, add Time Block duration floor - #211

Merged
WilliM233 merged 3 commits into
developfrom
feat/2C-04-expiry-defaults
Apr 27, 2026
Merged

WilliM233 merged 3 commits into
developfrom
feat/2C-04-expiry-defaults

Conversation

@WilliM233

Copy link
Copy Markdown
Owner

Summary

Realigns the seven EXPIRY_DEFAULTS to the v0.9 §Q4 authoritative table, introduces a block_duration floor for time_block_reminder (min(30m, block_duration)), and switches routine_checklist from a flat +4h to EOD-local — midnight at the start of the day after the notification's scheduled_date, computed in a new SERVER_TZ config setting. The PATCH-to-delivered handler now threads scheduled_date into calculate_expires_at so the EOD path is exercised in production.

SERVER_TZ is a Pydantic Settings field validated at startup via zoneinfo.ZoneInfo — a bad IANA key fails fast rather than silently producing wrong expiries. Added tzdata to requirements.txt because Windows hosts have no system tzdata and zoneinfo falls back to the PyPI package.

Closes #203

Changes

  • app/config.py — Added SERVER_TZ: str field (env var BRAIN3_SERVER_TZ, default America/Chicago), _validate_server_tz field validator, and server_tz property returning the resolved ZoneInfo.
  • .env.example — Documented the new BRAIN3_SERVER_TZ env var alongside the existing BRAIN3_APP_BEARER_TOKEN.
  • requirements.txt — Added tzdata==2026.2 as a runtime dep.
  • app/services/notification_defaults.py — Updated EXPIRY_DEFAULTS to v0.9 §Q4 values; added keyword-only scheduled_date and block_duration parameters to calculate_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 passes scheduled_date=notification.scheduled_date so routine_checklist hits the EOD path. (block_duration is not threaded — see Deviations.)
  • tests/test_notification_expiry.py — Constants and per-type calculation tests rewritten to assert v0.9 values. New TestTimeBlockFloor (5 tests) and TestRoutineChecklistEOD (4 tests) classes cover the new branches. test_habit_nudge_expires_approx_2h integration test renamed/rebanded to _approx_4h.

How to Verify

  1. git checkout feat/2C-04-expiry-defaults && pip install -r requirements-dev.txt
  2. pytest -v tests/test_notification_expiry.py — 58 tests pass; the TestTimeBlockFloor and TestRoutineChecklistEOD classes are the new behavior.
  3. pytest -v — full suite (1371 tests) passes.
  4. ruff check . — clean.
  5. Manual sanity: python -c \"from app.config import settings; print(settings.SERVER_TZ, settings.server_tz)\" → America/Chicago zoneinfo.ZoneInfo(key='America/Chicago').
  6. Negative sanity: BRAIN3_SERVER_TZ=Bogus/Zone python -c \"from app.config import settings\" raises ValueError: SERVER_TZ 'Bogus/Zone' is not a valid IANA zoneinfo key at startup.

Deviations

  • SERVER_TZ default is America/Chicago, not America/New_York as 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.
  • EOD-local acceptance-criteria test uses Chicago offset (-05:00 CDT), not the spec's example -04:00 Eastern. Same reason — the test is faithful to the intent (delivered 22:15 local on scheduled day → expires next-day 00:00 local in SERVER_TZ), adapted to the actual SERVER_TZ value. The spec's -04 example would only make sense if SERVER_TZ were Eastern. A separate test test_eod_local_uses_server_tz_setting monkeypatches SERVER_TZ to America/New_York and asserts the same shape, proving the calculation respects the setting.
  • Added tzdata to requirements.txt (out-of-spec but functionally required). Windows hosts have no system tzdata. Without tzdata, ZoneInfo('America/Chicago') raises ZoneInfoNotFoundError and app.config.settings = Settings() blows up at module load. Pinning tzdata==2026.2 matches the rest of the file's pinning style and gives deterministic TZ data across dev / CI / container.
  • block_duration is not threaded into the PATCH-to-delivered handler. No TimeBlock entity exists in the repo today; there is nothing to look up at delivery time. The parameter is wired into calculate_expires_at for the future time-block scheduler — when that scheduler lands, its creation path is the natural place to resolve and pass block_duration. Current time_block_reminder notifications 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

$ pytest -v
============================ 1371 passed in 21.14s ============================

$ ruff check .
All checks passed!

Touched-file detail (58/58):

$ pytest -v tests/test_notification_expiry.py
============================ 58 passed in 0.58s ===============================

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_nudge ignores 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 — monkeypatches SERVER_TZ to NY, asserts the calc honors it.
  • TestRoutineChecklistEOD::test_falls_back_to_default_when_scheduled_date_missing — defensive fallback.

Acceptance Checklist

  • tests/test_notification_expiry.py updated to assert v0.9 values.
  • New test: time_block_reminder with block_duration=10min yields expiry of delivered_at + 10min, not delivered_at + 30min.
  • New test: routine_checklist scheduled for 2026-04-20 and delivered at 22:15 local expires at 2026-04-21T00:00 local (not delivered_at + 4h which would cross into the next day). Adapted to SERVER_TZ=America/Chicago per L.
  • All existing expiry tests rewritten — deliberate behavior change.
  • SERVER_TZ setting accessible via settings.SERVER_TZ and consumed by calculate_expires_at.

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.
@WilliM233
WilliM233 merged commit d091b5d into develop Apr 27, 2026
2 checks passed
@WilliM233
WilliM233 deleted the feat/2C-04-expiry-defaults branch April 27, 2026 18:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[2C-04] Align EXPIRY_DEFAULTS to v0.9 Q4, add SERVER_TZ config, add Time Block duration floor

1 participant