Agents will invent a start-of-day if you leave the digest window unspecified, and that guess double-counts late events. This case study turns a vague daily-digest request into a timezone-strict calculator and a pytest suite. You run those tests before any model is allowed to write the worker that sends mail. A free coding model and a free server can draft the worker later, but they are not the source of truth.
Background
"Send each tenant a daily digest of yesterday's events" looks like a small cron job with a DATE() filter. That sentence hides three decisions: which timezone defines yesterday, whether the end bound is exclusive, and where late rows go. An unconstrained agent usually picks UTC, uses inclusive SQL between, and then backfills late events into a second mail.
You do not need a large product to see the failure mode clearly. A single America/New_York tenant and one event at 04:10 UTC already expose the bug. This worked example stays that small on purpose, so you can copy the calculator without standing up SMTP.
The project name is digest-window, and it is a labeled exercise rather than a claimed production incident. You should treat every command below as something you run locally first. You should also refuse to feed an assistant production event rows while you are still proving the window math.
Goal
You will ship a pure function that maps clock time, tenant timezone, and event time onto a window id. Missing timezones must fail closed, because a UTC default slides late-night events into the wrong digest. The worker, if an agent writes it, may only call that function and must not inline local date arithmetic.
You will keep a decision table next to the tests so reviewers can see calendar rules without reading Python. Success is a green pytest run plus a worker that imports the calculator. Success is not a polished HTML email template.
The window contract
Write the rules as data before you open a chat with any coding model. If a rule lives only in the prompt, the next generation will simplify it away. Freeze the table in git, then let tools write everything that is not the table.
Rules you freeze
- Every tenant stores an IANA timezone; empty values raise
MissingTimezoneError. - A digest covers the previous complete local day, not the last twenty-four hours of UTC.
- The window is half-open:
start_utc <= event < end_utc. - Events at or after
end_utcarelate_for_this_windowand never silently merged. - Naive datetimes are rejected; both
nowandevent_timemust be timezone-aware. - The worker writes
window_idinto logs and into the email header for later audits.
Decision table
| now_utc | tenant tz | event_time_utc | expected membership |
|---|---|---|---|
2026-03-09T12:00:00Z |
America/New_York |
2026-03-08T16:00:00Z |
in_window for 2026-03-08
|
2026-03-09T12:00:00Z |
America/New_York |
2026-03-09T08:00:00Z |
late_for_this_window |
2026-03-09T12:00:00Z |
(empty) | 2026-03-08T16:00:00Z |
MissingTimezoneError |
2026-03-09T06:30:00Z |
America/New_York |
2026-03-08T06:30:00Z |
still exclusive across the DST spring-forward |
United States clocks spring forward on 8 March 2026, so that local Sunday lasts twenty-three hours. Your tests should pin that civil date so an agent cannot "fix" the window with epoch division. If a generated change deletes the DST fixture, you reject the change even when the happy-path test still passes.
Implementation
Keep the calculator in one module with no HTTP, no database, and no SMTP client. That boundary is the point of the case study, because agents love to fold date math into the worker. You review the import graph as strictly as you review the arithmetic. You should also keep Window frozen so callers cannot mutate end_utc after the query starts.
# digest_window.py
from __future__ import annotations
from dataclasses import dataclass
from datetime import date, datetime, time, timedelta
from zoneinfo import ZoneInfo
class MissingTimezoneError(ValueError):
"""Raised when a tenant has no IANA timezone."""
@dataclass(frozen=True)
class Window:
timezone: str
start_utc: datetime
end_utc: datetime # exclusive
window_id: str
def require_timezone(tz_name: str | None) -> ZoneInfo:
if not tz_name or not str(tz_name).strip():
raise MissingTimezoneError("tenant timezone is required")
return ZoneInfo(tz_name)
def window_for_local_date(tz_name: str, local_date: date) -> Window:
tz = require_timezone(tz_name)
start_local = datetime.combine(local_date, time.min, tzinfo=tz)
next_local = datetime.combine(local_date + timedelta(days=1), time.min, tzinfo=tz)
start_utc = start_local.astimezone(ZoneInfo("UTC"))
end_utc = next_local.astimezone(ZoneInfo("UTC"))
window_id = f"{tz_name}:{local_date.isoformat()}"
return Window(
timezone=tz_name,
start_utc=start_utc,
end_utc=end_utc,
window_id=window_id,
)
def membership(event_time_utc: datetime, window: Window) -> str:
if event_time_utc.tzinfo is None:
raise ValueError("event_time_utc must be timezone-aware")
event = event_time_utc.astimezone(ZoneInfo("UTC"))
if window.start_utc <= event < window.end_utc:
return "in_window"
if event >= window.end_utc:
return "late_for_this_window"
return "before_window"
def digest_window_id(now_utc: datetime, tz_name: str) -> str:
tz = require_timezone(tz_name)
if now_utc.tzinfo is None:
raise ValueError("now_utc must be timezone-aware")
local_now = now_utc.astimezone(tz)
local_date = local_now.date() - timedelta(days=1)
return window_for_local_date(tz_name, local_date).window_id
The half-open interval is the line that saves you from double sends. Inclusive BETWEEN queries look friendly, then emit two digests for the same midnight instant. Reject the SQL shape below in review, even if an assistant generated it with confident comments.
-- Review reject: UTC DATE() ignores tenant timezone and exclusive bounds.
SELECT *
FROM events
WHERE DATE(created_at) = DATE(UTC_TIMESTAMP() - INTERVAL 1 DAY);
Tests you run before the worker exists
Put the decision table into pytest first. An agent that arrives later has to satisfy these names, not invent friendlier ones. Pin civil dates in 2026 so the suite does not drift when you rerun it next month.
# test_digest_window.py
from datetime import date, datetime
from zoneinfo import ZoneInfo
import pytest
from digest_window import (
MissingTimezoneError,
digest_window_id,
membership,
window_for_local_date,
)
UTC = ZoneInfo("UTC")
NY = "America/New_York"
def test_previous_local_day_window_id():
now = datetime(2026, 3, 9, 12, 0, tzinfo=UTC)
assert digest_window_id(now, NY) == "America/New_York:2026-03-08"
def test_event_in_window():
window = window_for_local_date(NY, date(2026, 3, 8))
event = datetime(2026, 3, 8, 16, 0, tzinfo=UTC)
assert membership(event, window) == "in_window"
def test_late_event_not_merged():
window = window_for_local_date(NY, date(2026, 3, 8))
event = datetime(2026, 3, 9, 8, 0, tzinfo=UTC)
assert membership(event, window) == "late_for_this_window"
def test_missing_timezone_fails_closed():
now = datetime(2026, 3, 9, 12, 0, tzinfo=UTC)
with pytest.raises(MissingTimezoneError):
digest_window_id(now, "")
def test_naive_event_is_rejected():
window = window_for_local_date(NY, date(2026, 3, 8))
with pytest.raises(ValueError):
membership(datetime(2026, 3, 8, 16, 0), window)
def test_dst_spring_forward_exclusive_end():
window = window_for_local_date(NY, date(2026, 3, 8))
assert window.start_utc == datetime(2026, 3, 8, 5, 0, tzinfo=UTC)
assert window.end_utc == datetime(2026, 3, 9, 4, 0, tzinfo=UTC)
on_end = datetime(2026, 3, 9, 4, 0, tzinfo=UTC)
assert membership(on_end, window) == "late_for_this_window"
Commands
python -m venv .venv
# Windows: .venv\Scripts\activate
source .venv/bin/activate
pip install pytest
pytest -q test_digest_window.py
You should see a green run before anyone generates digest_worker.py. If a model rewrites the calculator to make a new assertion pass, you revert the calculator and fail the change. The tests own the calendar; the model does not.
A worker shape that agents keep trying to skip
Paste this sketch only after the calculator tests are green. The sketch exists so the model cannot mint a second copy of the window math. Treat it as a proposed, unexecuted outline until test_digest_window.py stays green on your machine.
# Proposed worker sketch — unexecuted until test_digest_window.py stays green.
from datetime import timedelta
from zoneinfo import ZoneInfo
from digest_window import (
MissingTimezoneError,
digest_window_id,
membership,
window_for_local_date,
)
def run_digest(tenant, now_utc, fetch_events, send_mail, log):
try:
window_id = digest_window_id(now_utc, tenant.timezone)
except MissingTimezoneError:
log("skip_missing_timezone", tenant_id=tenant.id)
return "skipped"
local_yesterday = now_utc.astimezone(ZoneInfo(tenant.timezone)).date() - timedelta(days=1)
window = window_for_local_date(tenant.timezone, local_yesterday)
events = fetch_events(tenant.id, window.start_utc, window.end_utc)
in_window = [e for e in events if membership(e.created_at, window) == "in_window"]
late = [e for e in events if membership(e.created_at, window) == "late_for_this_window"]
log("digest_counts", window_id=window_id, in_window=len(in_window), late=len(late))
send_mail(tenant, window_id, in_window)
return window_id
If the generated file imports datetime and computes date.today() itself, you throw the file away. That is a review rule, not a style preference. Late events may be logged, but they must not ride along in yesterday's mail.
Review checklist
- Calculator module has no SMTP, HTTP, or ORM imports.
- Tests pin
2026-03-08inAmerica/New_York, notdate.today(). - Worker imports
digest_windowand does not calldate.today(). - Logs include
window_idand a late-event count. - Empty timezone fails closed before
fetch_events.
Where a remote model and a free server fit
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
You can stop at the calculator and never touch an assistant. If you want help drafting the worker, an open-source coding assistant is useful only after these tests exist. MonkeyCode offers free model access and a free server option, which is enough to generate digest_worker.py and run pytest off your laptop.
Give the assistant the decision table, the calculator module, and the pytest file as the prompt. Ask it for the worker and for log lines that include window_id. Do not ask it to "just make the digest work," because that wording invites UTC defaults and inclusive SQL. Keep production events off the free server; the fixtures in this article are synthetic on purpose, and a shared runner is the wrong place for tenant mail bodies.
Results from the worked fixtures
On 2026-03-09T12:00:00Z, a New York tenant's digest id is America/New_York:2026-03-08. An event at 2026-03-08T16:00:00Z is in window, which is 11:00 local time the day before the job. An event at 2026-03-09T08:00:00Z is late, even though a UTC DATE() filter might still label it the ninth.
The empty timezone fixture raises before any query runs, and that failure is the CI result you want. A quiet UTC fallback would have been a false green on the same input. None of these outcomes are runtime benchmarks; they are contract checks you can re-run on any machine with tzdata.
For the DST row, local 8 March 2026 in New York starts at 2026-03-08T05:00:00Z and ends at 2026-03-09T04:00:00Z. The twenty-three hour length is expected, and the exclusive end still holds at 04:00 UTC. If your host tzdata cannot reproduce those two instants, you fix the image before you trust a generated worker.
Limitations
This calculator does not send email, retry SMTP, or deduplicate overlapping cron launches. It does not track political tzdata changes beyond whatever zoneinfo loads from the host. A free server image with stale tzdata can disagree with your laptop on a newly announced offset.
You should not reuse this module for sub-hour tumbling windows, fiscal calendars, or "user's current travel timezone" products. Those need a different contract, and an agent will still import this file if you do not block it. Exclusive daily windows are the only behavior under test here.
Who should not use this workflow
Skip the remote server if your fixtures contain real addresses, message bodies, or tenant identifiers. Skip agent-written workers if nobody on the team can read the timezone tests and explain the half-open bound. Skip the whole pattern if your digest is a rolling twenty-four hour window, because previous-local-day is the wrong primitive.
Lessons learned
Ambiguous time language is a spec bug, and coding models amplify it instead of inventing a careful calendar. Exclusive end bounds belong in tests, not in a comment that the next prompt will drop. A free model is a decent typist for boilerplate workers once membership rules already fail closed.
A free server is a convenient pytest runner, not a production cron host, and not a reason to skip the table. Keep the calculator human-owned even when the worker is generated. When the suite is green, you can park the worker draft on MonkeyCode's free server option and keep the calculator on your machine.
Top comments (0)