DEV Community

Dakota Huang
Dakota Huang

Posted on

A Cache Helper Moves Only After Nine Frozen Rows

A messy freshness check should not be rewritten in one pass. Freeze the observed outputs before any cleanup begins. Then move one comparison inside a separate commit.

Why the freeze comes first

A cleaner function can still change a boundary by accident. Characterization tests record current outputs for fixed inputs. Those rows become the gate for the smallest edit.

This workflow fits a messy helper with few owners. It does not fit a public cache library release. It also does not replace a protocol review.

What the toy actually is

The module below is a labeled unexecuted teaching example. It is not a complete implementation of HTTP caching. No production traffic was used to build these rows.

RFC 9111 defines freshness lifetime and age calculations in detail. This toy skips resident time, heuristic freshness, and clock skew. Read that specification before changing a real cache.

The primary source used here is the RFC Editor copy of RFC 9111. Local names in the toy are not protocol field names. Follow the link before treating any row as compliant.

RFC 9111

The current branches

Three branches decide freshness in the current helper. The first branch runs only when both age fields exist. The second branch can ignore max-age when the age header is absent.

def is_response_fresh(now, stored_at, max_age, age_header, expires_at):
    # Current behavior under test. Not an RFC 9111 implementation.
    if max_age is not None and age_header is not None:
        age = age_header if age_header >= 0 else 0
        return age < max_age
    if expires_at is not None:
        return stored_at < expires_at
    if max_age is not None:
        elapsed = now - stored_at
        return elapsed < max_age
    return False
Enter fullscreen mode Exit fullscreen mode

Label this function as the behavior under test. Do not treat the comment as a finished spec. The tests below define the contract for this pass.

Step 1: List every caller

Search the repository for every caller before the edit. A missed caller can hide a depended-on quirk. Record the file path next to each search hit.

grep -R -n is_response_fresh --exclude-dir=tests .
Enter fullscreen mode Exit fullscreen mode

Keep the search output with the pull request notes. A later reader still needs that caller list. Do not assume the helper has only one site.

Step 2: Build nine frozen rows

Nine rows are enough for this small helper. Each row names the inputs and the current output. Add a short note when the output looks wrong.

id now stored_at max_age age_header expires_at fresh note
1 100 90 60 10 null true age under max-age
2 100 90 60 60 null false age equals max-age
3 100 90 60 -5 null true negative age clamped
4 100 90 0 0 null false zero age is not fresh
5 200 90 null null 100 true compares stored_at, not now
6 100 80 30 null null true elapsed under max-age
7 100 70 30 null null false elapsed equals max-age
8 100 90 5 null 200 true expires wins over elapsed
9 10 10 null null null false no lifetime fields

Verify each expected value by tracing the function once. Do not copy expected values from memory alone. If a row fails, correct the table before any edit.

All fixture numbers are plain integers in one unit. The helper does not convert seconds into milliseconds. Mixing units would make these frozen rows meaningless.

Step 3: Lock the rows in pytest

The characterization test should call the real function. It should not reimplement any of the branches. A copied formula would bless the cleanup by construction.

import pytest
from freshness import is_response_fresh

CASES = [
    # id, now, stored_at, max_age, age_header, expires_at, expected
    (1, 100, 90, 60, 10, None, True),
    (2, 100, 90, 60, 60, None, False),
    (3, 100, 90, 60, -5, None, True),
    (4, 100, 90, 0, 0, None, False),
    (5, 200, 90, None, None, 100, True),
    (6, 100, 80, 30, None, None, True),
    (7, 100, 70, 30, None, None, False),
    (8, 100, 90, 5, None, 200, True),
    (9, 10, 10, None, None, None, False),
]

@pytest.mark.parametrize('row', CASES, ids=[str(item[0]) for item in CASES])
def test_current_freshness_rows(row):
    _row_id, now, stored_at, max_age, age_header, expires_at, expected = row
    actual = is_response_fresh(
        now, stored_at, max_age, age_header, expires_at
    )
    assert actual is expected
Enter fullscreen mode Exit fullscreen mode

Run one command and save the printed result. Nine passed tests mean the freeze currently holds. A failure means the table and the code disagree.

python -m pytest tests/test_freshness_rows.py -q
Enter fullscreen mode Exit fullscreen mode

This command is the gate for the next edit. Do not replace it with a manual spot check. Spot checks miss the equal-boundary rows in this table.

Step 4: Make the smallest safe change

The safe change only extracts the negative-age clamp. It does not touch the expires comparison at all. It does not change the strict less-than operator.

def clamp_observed_age(age_header):
    if age_header >= 0:
        return age_header
    return 0


def is_response_fresh(now, stored_at, max_age, age_header, expires_at):
    if max_age is not None and age_header is not None:
        age = clamp_observed_age(age_header)
        return age < max_age
    if expires_at is not None:
        return stored_at < expires_at
    if max_age is not None:
        elapsed = now - stored_at
        return elapsed < max_age
    return False
Enter fullscreen mode Exit fullscreen mode

Label the new helper as a move, not a fix. Callers should still see the same boolean results. Row 3 still expects a clamped negative age.

Step 5: Re-run the same gate

Run the identical pytest command after the extraction. All nine rows must remain green after this move. Any red row should stop the refactor immediately.

Inspect the diff and confirm only the clamp moved. A wider diff is not the smallest safe change. Split unrelated edits before running the gate again.

git diff -- freshness.py
Enter fullscreen mode Exit fullscreen mode

If a row changes, revert the extracted helper. Do not update the expected value to match new code. That update would hide an accidental behavior change.

A bad extraction often replaces less-than with less-or-equal. Rows 2, 4, and 7 would flip from false to true. That flip is the signal to revert, not to edit expectations.

A second failure mode moves the expires check later. Row 8 would then use elapsed and return false. The current row expects true because expires is checked first.

Step 6: Park the real bug

Row 5 stays fresh even when now is past expires. That result comes from comparing stored_at with expires_at. A behavior fix belongs in a later named change.

Write the desired outcome beside the frozen row. Do not replace the frozen row in this pass. A separate test name should mark the intended change.

import pytest

@pytest.mark.skip(reason='proposal only; enable in a later change')
def test_expires_should_compare_now_later():
    now, stored_at, expires_at = 200, 90, 100
    assert now < expires_at
Enter fullscreen mode Exit fullscreen mode

Keep that proposal skipped until a later review. Enabling it now would mix two different commits. The freeze and the fix need different diffs.

Where free model access fits

Disclosure: This article was prepared as part of MonkeyCode's product outreach. A free model can draft the clamp extraction from frozen rows. A free server can run the same pytest command on that diff.

Neither claim names a model, quota, duration, or machine size. Confirm current free-tier terms before relying on either option. This article does not state quotas, uptime, or model names.

Availability here means free model access and a free server option. The operator still has to review every changed line. The model does not decide which quirk is a bug.

The server does not prove the table was complete. Use either option only after the rows exist. A model without fixtures may simplify the expires branch.

That simplification is a product change, not a refactor. Generated text is still an unexecuted proposal until pytest runs. Save the command output next to the diff.

Decision table for the next edit

Signal Stay in the refactor Open a behavior change
Nine rows stay green Yes No
A boolean flips No Yes, after a new test
Only the clamp moves Yes No
Expires starts using now No Yes, separate commit
Caller list is incomplete Stop Stop

Read this table before accepting a generated diff. Green tests plus a flipped boolean mean stop. An incomplete caller list also means you should stop.

Limits of this gate

Characterization tests preserve the current bugs on purpose. They do not certify compliance with RFC 9111. They do not cover callers that were never found.

Equal boundaries are included because off-by-one edits are common. Missing headers are included because they change the branch. Nine rows still miss concurrency and clock steps.

Free model access is not a promise of one named model. Free server access is not a promise of duration or capacity. Those options only cover drafting and running this gate.

Do not use this approach for a shared cache library. Do not use it when the rows cannot be rerun. Do not use it to justify a silent freshness fix.

Review checklist

  1. Confirm the caller search was saved with the notes.
  2. Confirm all nine rows were traced before the edit.
  3. Confirm the diff only extracts the age clamp.
  4. Confirm pytest was re-run on the same file.
  5. Confirm the expires fix stayed out of the diff.

Each numbered check is a required review question. A missing check should block the merge entirely. The questions stay useful without any product tool.

A free MonkeyCode server can run this same pytest file. Read the resulting diff locally before any merge.

Closing

The conclusion of this characterization pass stays narrow. Freeze nine outputs, then extract one age clamp. Leave the expires comparison for a later named change.

Top comments (0)