A Test That Passes by Day and Fails at Dawn
A date filter test failed every dawn from midnight to seven: two clocks in one test. Fix: derive dates from the database clock, pin midday.
TL;DR
A scheduled test failure came from two clocks: Go on a WIB host and MySQL in UTC, seven hours apart. Around midnight their dates diverged, so Go's yesterday still contained the database's today and assertions failed until morning. The fix uses MySQL's CURDATE as the single witness and pins rows to midday to stay clear of boundary drift.
The failure pattern was too neat to be a coincidence: this date filter test passed whenever it ran from midday through the evening, then failed again every dawn, roughly from midnight until seven. Nothing in the code changed, no dependency moved, yet every morning the yesterday assertion missed by exactly one day. Both sides were deterministic, in fact; only the window was narrow. A test that fails randomly is annoying; one that fails on a schedule like this suggests another clock is running somewhere. And that was precisely it: there were two clocks in that test, and I was the one who let them in.
Two Clocks in a Single Test
The first clock came from Go. The test derived yesterday's expected date from time.Now(), and that clock was never neutral: Local follows the system zone, on Unix it reads the TZ variable, and when that is empty it falls back to /etc/localtime [7]. The test process ran on a WIB host, so Go's "yesterday" flipped to a new date at midnight WIB.
The second clock came from MySQL. The seeded row was pinned with NOW() inside the SQL statement, and values from functions like that are zone sensitive: they follow the server session zone, which defaults to the server system zone [6]. The test database ran in a UTC container. So the database's "now" trailed Go's "now" by seven hours.
Each was individually correct, and that is exactly where the fragility lived. From five in the afternoon WIB, UTC had already flipped its date, and until midnight WIB the container's date stayed ahead of the host's. During that window, Go's "yesterday" still contained the database's "today". The daily filter caught a row the test said should not exist, the assertion collapsed, and at seven in the morning everything was normal again without me touching anything.
The Clock Under Test Is the Only Witness
The fix was not to tame one clock until it matched the other, but to pick a single witness: the clock of the party being tested. The date filter is evaluated by MySQL, so the expected dates had to be born from MySQL, not from the Go process:
SELECT DATE_FORMAT(CURDATE(), '%Y-%m-%d');
UPDATE registration_submissions
SET created_at = DATE_SUB(?, INTERVAL 12 HOUR) WHERE id = ?;
CURDATE() returns today's date [5], and the DATE_FORMAT result is deliberately a string, so it stays safe under any connection configuration, especially around parseTime. From that one date, yesterday was computed on the Go side as a string, no longer from the process clock.
The second part matters even more: the seeded row was pinned to midday of the database-side date through bound parameters, DATE_SUB(?, INTERVAL 12 HOUR) for yesterday and DATE_ADD(?, INTERVAL 12 HOUR) for today [5]. Far from midnight in any zone, a seven hour skew between clocks can no longer drag the row onto the wrong date.
The boundary rule itself had been right all along: the filter's upper bound executes to as less-than-tomorrow, so today still counts. What lied first was not the filter but the comparison material.
The Rule I Wrote Down
This small incident became a standing rule in the project's implementation plan: any test touching dates must derive its expectations from the database clock, not from the process's time.Now(). The thing is, time.Now() is a process wall clock, a mix of wall and monotonic readings [1], and the wall side is shaped by the zone the process lives in. Containers may run UTC, hosts may run WIB, CI may run something else entirely; a test that does not choose its witness inherits all of those differences.
Having two clocks in one test is not redundancy, it is a conflict waiting for a schedule. Name the single witness, pin every timestamp to it, and keep them away from date boundaries. After that the test can run at two in the morning without drama. I have only proven it that far so far, but at least I am no longer afraid to try.