Pin the send decisions before any quiet-hour predicate moves. The smallest safe change is one named extraction. Wider edits are behavior changes, not harmless cleanup.
Start from outputs, not authors
Git blame names the last editor of a line. It never names the allowed Boolean return value. Frozen rows are the specification for this move.
A scheduler file often hides three separate concerns. Clock reads sit beside channel rules and priority bypasses. That mix makes a small cleanup look cheaper than it is.
One flipped comparison changes who receives a night message. The diff stat can still show two changed lines. Line count is not a safety metric here.
Scope of the proposed fixture
The listing below is a proposed local fixture. It is not a measured production incident report. Run it locally before you trust any generated diff.
The target is identical behavior after one extraction. Only the quiet-hour calculation changes its physical home. Branch order, constants, and return values stay fixed.
This pass does not redesign the quiet-hour window. It does not add user-local time zone support. It does not tune priority names or channel policy.
Current decisions worth pinning
Quiet hours run from 22:00 inclusive through 07:00 exclusive. A page priority bypasses quiet hours on every row. SMS may send at night when quiet mode is disabled.
An email digest may send while quiet mode is active. Low-priority push stays blocked even outside that window. Ordinary daytime mail still sends when no block matches.
These rows describe the function that exists today. They do not claim each bypass is a good product rule. Change the rule only after you replace the expected values.
Step 1: Freeze time inputs
Pass explicit datetime values into the function under test. Do not call datetime.now inside the characterization fixture. Use timezone.utc on every timestamp in the table.
A laptop in another offset must produce the same Booleans. Local wall clocks would split the suite across machines. UTC instants remove that particular source of drift.
Store the eight cases next to the function. Keep production code free of test-only clock calls. The fixture owns time, and the function only reads arguments.
Step 2: Record eight decision rows
Each case carries instant, channel, priority, quiet flag, and expectation. Eight rows cover both boundaries and the main bypasses. Add a row only when it protects a branch you might touch.
Row one checks 22:00 with quiet mode enabled for SMS. The result is false, because that instant is inside quiet hours. The page bypass is not active on that row.
Row two repeats 22:00 with an active page priority. The result is true, because the bypass runs first. Extraction must not sink that check below the quiet branch.
Row three checks 23:15 SMS with quiet mode disabled. The result is true, because the SMS exception still applies. Do not fold that exception into the new helper.
Row four checks 23:15 email digest with quiet mode enabled. The result is true, because digest mail is allowed at night. A helper that returns early would drop this send.
Row five checks 06:59 high-priority push with quiet mode enabled. The result is false, because 06:59 remains inside the window. The end boundary is exclusive, so this minute still blocks.
Row six checks 07:00 low-priority push with quiet mode enabled. Quiet mode is already inactive at exactly 07:00. The result is still false, because low-priority push is blocked later.
Row seven checks 07:00 normal email with quiet mode enabled. The result is true, because no later block matches. This row guards the exclusive end of the window.
Row eight checks 21:59 high-priority push with quiet mode disabled. The result is true, because 21:59 sits outside quiet hours. A greater-than mistake against 22:00 would flip this case.
Step 3: Lock the gate before editing
Run the suite before you move any line. Place both files in one directory before running unittest.
python -m unittest test_quiet_gate.py -v
python test_quiet_gate.py -v
Expect eight passing tests before opening the editor. A failing row means the fixture disagrees with the function. Repair the fixture or the setup, not the send policy.
Save the verbose output beside the patch notes. That log is the before-image for this refactor. Later failures should be compared with those same eight names.
Use subTest so one flipped row does not hide the rest. Eight names should be able to fail independently.
Python must be able to import quiet_gate from that directory. Treat the commands as the intended gate, not a recorded run.
The function before the move
from datetime import time
def should_send(now, channel, priority, quiet_enabled):
if priority == 'page':
return True
clock = now.timetz().replace(tzinfo=None)
quiet = clock >= time(22, 0) or clock < time(7, 0)
if channel == 'sms' and not quiet_enabled:
return True
if quiet_enabled and quiet:
return channel == 'email' and priority == 'digest'
if channel == 'push' and priority == 'low':
return False
return True
The quiet calculation is inline, which is the mess. The branch order is the behavior you must preserve. Do not reorder those returns while extracting the helper.
The characterization module
import unittest
from datetime import datetime, timezone
from quiet_gate import should_send
CASES = [
('2026-10-12T22:00:00+00:00', 'sms', 'normal', True, False),
('2026-10-12T22:00:00+00:00', 'sms', 'page', True, True),
('2026-10-12T23:15:00+00:00', 'sms', 'normal', False, True),
('2026-10-12T23:15:00+00:00', 'email', 'digest', True, True),
('2026-10-12T06:59:00+00:00', 'push', 'high', True, False),
('2026-10-12T07:00:00+00:00', 'push', 'low', True, False),
('2026-10-12T07:00:00+00:00', 'email', 'normal', True, True),
('2026-10-12T21:59:00+00:00', 'push', 'high', False, True),
]
class QuietGateTests(unittest.TestCase):
def test_frozen_send_rows(self):
for raw, channel, priority, quiet_enabled, expected in CASES:
with self.subTest(raw=raw, channel=channel, priority=priority):
now = datetime.fromisoformat(raw)
self.assertEqual(
now.utcoffset(),
timezone.utc.utcoffset(None),
)
got = should_send(now, channel, priority, quiet_enabled)
self.assertEqual(got, expected)
if __name__ == '__main__':
unittest.main()
Step 4: Apply the smallest extraction
Create in_quiet_hours and move only the clock comparison. Call that function once, in the same branch as today. Leave every surrounding conditional untouched by this pass.
from datetime import time
def in_quiet_hours(now):
clock = now.timetz().replace(tzinfo=None)
return clock >= time(22, 0) or clock < time(7, 0)
def should_send(now, channel, priority, quiet_enabled):
if priority == 'page':
return True
if channel == 'sms' and not quiet_enabled:
return True
if quiet_enabled and in_quiet_hours(now):
return channel == 'email' and priority == 'digest'
if channel == 'push' and priority == 'low':
return False
return True
Review the diff with a path-limited command. The helper file may change during this extraction. The test file should not change in this pass.
git diff -- quiet_gate.py
python -m unittest test_quiet_gate.py -v
A test edit during extraction is a warning, not a convenience. Keep the eight expected Booleans byte-for-byte stable. A green suite after a test edit proves nothing useful.
Step 5: Accept or reject the diff
Use a fixed table before you keep the patch. Accept only the first row of that table. Every other row belongs to a second refactor.
| Diff property | Decision | Reason |
|---|---|---|
| Helper extracted, branch order unchanged | Accept | Outputs stay pinned by eight rows |
| Comparison operator edited | Reject | The 07:00 or 22:00 row will flip |
| Page bypass moved below quiet check | Reject | Night pages would start failing |
| SMS exception folded into the helper | Reject | Disabled quiet mode would change |
| New timezone conversion added | Reject | Outside the frozen UTC table |
| A characterization row deleted | Reject | Deletion hides a behavior shift |
| Constants renamed during the move | Reject | Review noise without a behavior need |
Open a new characterization pass for that second goal. Rerun the same eight names between every later edit. Do not stack cleanups into the extraction commit.
Use a free model only as a drafter
A free coding model can propose the extraction text. MonkeyCode's free model access and free server option fit this drafting step. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Confirm current availability in the product before you rely on it. The suite on your machine remains the acceptance gate. No extra product limit is assumed by these steps.
Give the model the current function and the eight expected rows. Ask it to extract one helper and preserve branch order. Tell it that changing a Boolean is a failed draft.
Paste the resulting patch into your local tree. Run the same unittest command you ran before the edit. Keep the patch only when all eight names still pass.
Discard a draft that edits the test table to get green. Discard a draft that explains away a flipped row. Confidence in the reply is not an execution result.
You can use the free server option for the drafting session. Do not treat that session as the local acceptance gate. Re-run the suite even when the draft looks identical.
Limitations to keep in the patch notes
Characterization tests preserve defects as well as intent. The night page bypass remains, even if product later rejects it. Removing that bypass is a rule change, not this extraction.
The table uses UTC instants and ignores local zones. Daylight-saving folds sit outside these eight frozen rows. A user-local quiet window needs another frozen set.
Naive datetime inputs sit outside this frozen table. Pass aware UTC values, or the offset check fails. The helper still compares clock faces after dropping tzinfo.
The timetz call drops the offset before comparison. Two zones sharing a clock face would match today. Do not describe that behavior as internationalized support.
One unittest method with subtests still shares setup. There is no setup here, which keeps the file short. Add fixtures later only when a new input appears.
A green run does not prove thread safety or delivery success. The function returns a Boolean and performs no I/O. Downstream senders need their own separate characterization rows.
Who should not use this pass
Skip this pass when quiet-hour policy itself must change. Write the new expected Booleans before touching the branches. The old rows will correctly block an intentional change.
Skip this pass when the function reads a live clock. Skip it when a send depends on a network response. Introduce fakes for those inputs before any extraction.
Skip this pass when you cannot execute unittest locally. An unread patch is not a completed refactor. Wait until the eight names can fail on your laptop.
Skip this pass when several helpers must move together. Combined moves hide which edit flipped a row. Sequence them, and rerun the gate between each move.
Close the loop
Record that eight green rows authorized this extraction only. Leave the characterization module in the tree after merge. The next edit needs its own rows before it starts.
A MonkeyCode free model session can draft that one extraction. Let the local suite accept or reject the patch.
Top comments (0)