Split no surcharge helper until three quotes stay identical. Characterization tests store current cents, not hoped-for cents. The smallest safe change is one pure function with matching outputs.
A messy quote function hides several rules in one body. Zone bases, fuel percent, residential add-ons, and weekend flags share locals. A wide rewrite can change money while the diff looks tidy.
What this characterization gate is meant to protect
This workflow protects observed quote cents during one extract. It does not approve a new carrier rate card. It does not prove tax, duty, or address truth.
Use it when one function mixes pure math with side effects. Skip it when the next change must alter a customer-visible price. Skip it when the function calls a live rating API.
Record the contract before any code moves
The artifact is a three-row table plus one test file. Each row names inputs and the cents the current function returns. The table is the contract for the later extract.
| row | zone | weight_g | residential | weekend | cents |
|---|---|---|---|---|---|
| q1 | 2 | 400 | no | no | 920 |
| q2 | 2 | 400 | yes | no | 1310 |
| q3 | 5 | 1200 | yes | yes | 2970 |
Those cents come from the sample function below. They are not market rates and not a benchmark. Replace every number with output from your unchanged module before you edit.
Sample quote module used only as a proposal
The sample is a proposal you can paste into a scratch repo. It is not a production carrier integration or a live rate call. It keeps one global clock so the mess is visible.
from datetime import datetime, timezone
AS_OF = datetime(2026, 10, 11, tzinfo=timezone.utc)
def quote_cents(zone, weight_g, residential, weekend):
base = {2: 800, 5: 1400}[zone]
if weight_g > 1000:
base += 600
elif weight_g > 500:
base += 250
fuel = (base * 15) // 100
total = base + fuel
if residential:
total += 350 + ((weight_g + 99) // 100) * 10
if weekend:
total += 200
_ = AS_OF # leftover read; leave it during this extract
return total
Row q1 uses zone 2 and 400 grams. Base stays 800 and fuel is 120 cents. No surcharge applies, so the result is 920.
Row q2 adds the residential branch on the same base. The residential add-on is 350 plus 40 cents. That path returns 1310 cents for row q2.
Row q3 uses zone 5 and 1200 grams. Base becomes 2000 cents and fuel is 300. Residential adds 470 and weekend adds 200, so the result is 2970.
Check that arithmetic before you trust the test. A wrong expected value freezes a typo, not behavior. Recalculate each row on paper, then in the interpreter.
Numbered workflow
1. Freeze the three observed quote rows first
Write the table into the repo as data, not as comments. Keep the units visible inside each column name. Do not add a fourth row during this pass.
QUOTE_ROWS = [
{
'row': 'q1',
'zone': 2,
'weight_g': 400,
'residential': False,
'weekend': False,
'cents': 920,
},
{
'row': 'q2',
'zone': 2,
'weight_g': 400,
'residential': True,
'weekend': False,
'cents': 1310,
},
{
'row': 'q3',
'zone': 5,
'weight_g': 1200,
'residential': True,
'weekend': True,
'cents': 2970,
},
]
Commit this table only after a human checks each cent. A model draft does not replace that check. The file should fail review if any cent lacks a source run.
2. Add one characterization test around those rows
Place the function in shipping/quote.py and the rows in shipping/fixtures.py. Place the test in tests/test_quote_rows.py beside that package. Add an empty shipping/init.py so the imports resolve.
The test calls the public function and compares cents. It does not import private helpers that do not exist yet. It does not mock the result it is trying to pin.
import pytest
from shipping.quote import quote_cents
from shipping.fixtures import QUOTE_ROWS
@pytest.mark.parametrize('row', QUOTE_ROWS, ids=lambda row: row['row'])
def test_quote_rows_stay_put(row):
got = quote_cents(
zone=row['zone'],
weight_g=row['weight_g'],
residential=row['residential'],
weekend=row['weekend'],
)
assert got == row['cents']
Run that test on the untouched function first. A red run means the table is wrong. It does not mean the module needs a rewrite.
Use python -m pytest tests/test_quote_rows.py -q for the gate. Repeat that same command after every later edit. Do not swap in a broader suite for this gate.
Stop the extract when that test is red. Fix the table or the call shape, then rerun. Do not start an extract against a failing pin.
3. Extract only the residential surcharge helper now
The safe change extracts only the residential add-on. Zone lookup, fuel percent, and weekend flag stay in place. The leftover clock read also stays in place.
def residential_surcharge_cents(weight_g):
return 350 + ((weight_g + 99) // 100) * 10
def quote_cents(zone, weight_g, residential, weekend):
base = {2: 800, 5: 1400}[zone]
if weight_g > 1000:
base += 600
elif weight_g > 500:
base += 250
fuel = (base * 15) // 100
total = base + fuel
if residential:
total += residential_surcharge_cents(weight_g)
if weekend:
total += 200
_ = AS_OF
return total
This extract is behavior-preserving only if the formula matches the old line. Keep integer division instead of float rounding here. Do not switch to bankers rounding in the same edit.
4. Reject any diff that moves other rules
Use a short decision table before you keep a patch. The columns are signals you can see in the diff. The action stays binary: keep the patch or revert.
| Signal in the diff | Action |
|---|---|
| Only residential_surcharge_cents is new | Keep and rerun the three rows |
| Fuel percent or zone map changed | Revert |
| A new zone or weight band appeared | Revert |
| AS_OF was deleted or reassigned | Revert |
| Expected cents in the table changed | Revert unless product signed that price change |
A green test with a mutated table is not a pass. Compare the fixture file to the previous commit. The three cent values must be byte-for-byte stable.
Inspect the extract with git diff on shipping/quote.py. Confirm the helper is the only new symbol. Reject the patch if the zone table moves.
5. Rerun the same three-row test after the move
Run python -m pytest tests/test_quote_rows.py -q again after the extract. Require the same three ids q1, q2, and q3. Do not add coverage flags that hide a skip.
If one row fails, restore the inlined formula. Do not fix forward with a new expected cent. The gate measures output stability, not code elegance.
Fill each fixture row from the unchanged function
Fill real rows by calling the unchanged function, not by guessing. Print one row at a time and copy the integer. Store the printout next to the fixture until review ends.
def capture_row(row_id, zone, weight_g, residential, weekend):
cents = quote_cents(zone, weight_g, residential, weekend)
return {
'row': row_id,
'zone': zone,
'weight_g': weight_g,
'residential': residential,
'weekend': weekend,
'cents': cents,
}
The capture helper above is a proposal, not a required dependency. It returns a dict that matches the fixture keys. Delete it after the table is committed if you prefer less code.
python -c 'from shipping.quote import quote_cents; print(quote_cents(2, 400, False, False))'
Expect 920 from that one-line call on the sample. Expect 1310 when the residential flag is true. Expect 2970 for zone 5, 1200 grams, and both flags.
Read failure signatures before you edit again
A q2-only failure means the residential formula drifted. A zone KeyError means the map was touched. A change from 920 to 9.20 means a float leaked in.
Treat each signature as a revert signal, not a debate. Restore the prior function before you edit again. Rerun the three rows before you open a new diff.
Where free model access and a free server fit
Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode's free model access can draft the test file from the table. MonkeyCode's free server option can run that same pytest command on a clean checkout.
Both are optional tools around the gate, not a substitute for the table. Paste the current function and the three rows. Ask only for a characterization test and one helper extract.
Tell the model to refuse new zones, new percents, and new expected cents. Treat the reply as an unexecuted proposal until you review it. Check each assertion against the fixture table yourself.
Discard any row the model invented to look complete. Then run the reviewed test on the free server checkout. The point is a second machine, not a claimed spec or quota.
If that server run is unavailable, keep the local green result. Wait for a clean checkout instead of inventing a pass. Do not send secrets, live carrier keys, or customer addresses in the prompt.
The sample uses fake zones and integer cents. Your real fixture should use the same restraint. Strip account ids before the table leaves the repo.
Limits that the frozen quote table cannot hide
A characterization test locks current behavior, including known bugs. If 1310 cents is already wrong, this gate will protect the error. Price corrections need a separate decision and a changed expected value.
Three rows do not cover zone 9, zero weight, or negative flags. The extract can still break an untested branch. Add rows only after this change is green, in a later pass.
The sample ignores leap seconds, currency, and carrier minimums. Do not cite these cents outside the tutorial. They exist so the arithmetic can be checked by hand.
Free model output can rename fields and still look plausible. Free server access does not review the diff for you. Neither claim states a model name or a hardware size.
Neither claim states a time limit or a permanent quota. Do not infer uptime, region, or retention from this article. Verify current product terms before you rely on availability.
Who should skip this characterization path for now
Do not use this path when the ticket changes a price on purpose. A pin against the old price will block the required edit. Write the new expected cents first, with a product note.
Do not use this path when quote_cents opens a network socket. A live network call makes the row non-deterministic. Stub the edge or drop that row from the gate.
Do not use this path if nobody can recompute the three results. Unread fixtures become quiet folklore within one release. A later editor will fix them without noticing the shift.
Review the helper once before you merge
Read the helper and the call site side by side. Confirm the residential formula moved once and stayed intact. Confirm fuel, zones, weekend, and AS_OF did not move.
If you already have a free MonkeyCode server, rerun pytest there before merge. Keep the local fixture check in place either way.
The conclusion of this characterization gate stays narrow. Three frozen quotes can gate one surcharge move. They cannot bless a full quote module rewrite.
Top comments (0)