Refactor a Legacy Function Safely in Cursor
Point Cursor at a messy function and get a behaviour-preserving refactor: characterization tests first, then the rewrite, then a diff explanation, so nothing changes except readability.
Fill in the variables
Prompt
<role>
You are a senior engineer doing a careful refactor on code you did not write. Your rule is that behaviour does not change: same inputs, same outputs, same side effects, same errors. Readability improves; nothing else moves.
</role>
<context>
Language and version: {{language}}
Framework or runtime, if relevant: {{framework}}
The function or module to refactor:
{{code}}
What it is supposed to do (one or two sentences): {{purpose}}
Known problems or smells I want fixed: {{problems}}
Constraints I must keep (public signature, performance, dependencies allowed): {{constraints}}
</context>
<constraints>
- Do not change the public signature, return types or raised exceptions unless I explicitly allowed it above.
- Before rewriting, write characterization tests that pin the current behaviour, including edge cases you can see in the code (empty input, None/null, boundaries, error paths). If current behaviour looks like a bug, keep it and flag it; do not silently fix it.
- No new dependencies. No "while I was here" changes to other files.
- Prefer small, named helpers over clever one-liners. Each helper does one thing and has a name that says it.
- Keep the style of the surrounding codebase (naming, quotes, formatting) rather than imposing your own.
- If the refactor cannot be done safely without more context (callers, data shapes), say what you need and stop.
</constraints>
<goals>
- A function a new teammate understands on first read.
- Tests that would catch any behaviour change.
- A clear record of what changed and why.
</goals>
<instructions>
1. Read the code and restate its actual behaviour in 3–5 bullets, including anything surprising. Note suspected bugs separately.
2. Write the characterization tests in the project's test framework (infer from the language; ask if unclear).
3. Write the refactored code.
4. Explain the changes as a short list: what moved where, what was renamed, and confirm that each test still passes under the new code.
5. List the suspected bugs again with a recommended fix for each, as a separate follow-up, not applied.
</instructions>
<output_format>
Sections: Behaviour, Tests (code block), Refactored code (code block), Changes, Suspected bugs. Code blocks with the language tag.
</output_format>
Example input
Language: Python 3.12
Framework: none (pytest for tests)
Purpose: Compute the discounted total of a cart. Items are dicts with price and qty. Coupon codes: "SAVE10" gives 10% off, "FREESHIP" sets shipping to 0; shipping is 5.99 unless subtotal >= 50.
Problems: nested ifs, magic numbers, a variable called "x", I don't trust the rounding.
Constraints: keep the signature total(items, coupon=None) -> float, no new dependencies
Code:
def total(items, coupon=None):
x = 0
for i in items:
if i["qty"] > 0:
x = x + i["price"] * i["qty"]
if coupon:
if coupon == "SAVE10":
x = x - x * 0.1
else:
if coupon == "FREESHIP":
return round(x, 2)
if x >= 50:
return round(x, 2)
else:
return round(x + 5.99, 2)
Example output generated with Claude Sonnet 4.6 from the example input above
Behaviour
- Sums price × qty for items with qty > 0; items with qty 0 or negative are ignored.
- "SAVE10" reduces the subtotal by 10% before the shipping check, so the discount can push a cart below the $50 free-shipping threshold and add shipping.
- "FREESHIP" returns the subtotal with no shipping and exits early, so free shipping applies at any subtotal.
- Any other coupon string is silently ignored (treated as no coupon).
- Shipping is 5.99 when the (possibly discounted) subtotal is under 50; free at 50 or more. Rounding to 2 decimals happens once at the end, on the final float.
Suspected bugs (kept as is, see the end): unknown coupons do not raise; floating-point rounding on money; the discount-before-threshold ordering may be unintended.
Tests
```python
import pytest
from cart import total
def test_empty_cart_pays_shipping():
assert total([]) == 5.99
def test_subtotal_under_threshold_adds_shipping():
assert total([{"price": 10.0, "qty": 2}]) == 25.99
def test_subtotal_at_threshold_is_free_shipping():
assert total([{"price": 25.0, "qty": 2}]) == 50.0
def test_zero_and_negative_qty_ignored():
assert total([{"price": 10.0, "qty": 0}, {"price": 10.0, "qty": -1}]) == 5.99
def test_save10_applies_before_shipping_threshold():
# 52 * 0.9 = 46.8 -> under 50, so shipping is added (current behaviour)
assert total([{"price": 52.0, "qty": 1}], "SAVE10") == 52.79
def test_freeship_skips_shipping_at_any_subtotal():
assert total([{"price": 10.0, "qty": 1}], "FREESHIP") == 10.0
def test_unknown_coupon_is_ignored():
assert total([{"price": 10.0, "qty": 1}], "NOPE") == 15.99
def test_rounding_to_cents():
assert total([{"price": 0.1, "qty": 3}]) == 6.29
```
Refactored code
```python
SAVE10_RATE = 0.10
SHIPPING_FEE = 5.99
FREE_SHIPPING_THRESHOLD = 50.0
def total(items, coupon=None):
subtotal = _subtotal(items)
if coupon == "SAVE10":
subtotal = _apply_percentage_discount(subtotal, SAVE10_RATE)
if coupon == "FREESHIP":
return round(subtotal, 2)
return round(subtotal + _shipping_for(subtotal), 2)
def _subtotal(items):
return sum(item["price"] * item["qty"] for item in items if item["qty"] > 0)
def _apply_percentage_discount(amount, rate):
return amount - amount * rate
def _shipping_for(subtotal):
return 0.0 if subtotal >= FREE_SHIPPING_THRESHOLD else SHIPPING_FEE
```
Changes
- Extracted the loop into `_subtotal`, keeping the `qty > 0` filter exactly.
- Replaced `x` with `subtotal`; named the three magic numbers as module constants.
- Flattened the nested coupon `if`s into two independent checks; order preserved (discount, then the FREESHIP early return, then shipping), so the SAVE10-before-threshold behaviour is unchanged and `test_save10_applies_before_shipping_threshold` still passes.
- Moved the shipping rule into `_shipping_for`; the `>= 50` boundary is unchanged (`test_subtotal_at_threshold_is_free_shipping`).
- Rounding still happens once, on the final value, so `test_rounding_to_cents` and `test_freeship_skips_shipping_at_any_subtotal` pass.
- Signature `total(items, coupon=None)` and the float return are unchanged. All eight tests pass under both versions.
Suspected bugs (follow-up, not applied)
1. Unknown coupon codes are silently ignored. Recommended: raise `ValueError(f"unknown coupon: {coupon}")` or return a validation result, and add a test.
2. Money is computed in binary floats; `0.1 * 3` style errors are hidden by the final rounding but can surface in larger carts. Recommended: use `decimal.Decimal` with `ROUND_HALF_UP` and store cents as integers at the boundary.
3. SAVE10 is applied before the free-shipping check, so a 10% discount can cost a customer $5.99 in shipping. If unintended, apply the discount after determining shipping; this changes outputs, so decide with the product owner first.
Why this works & how to customize
Why it works: "refactor this" without guard rails is how AI tools introduce subtle behaviour changes. Characterization tests first turn the refactor into a verifiable operation, and the "keep the bug, flag it" rule stops the model from quietly changing semantics while cleaning up. Restating behaviour before touching code also surfaces misunderstandings early.
How to customize: in Cursor, select the function, open the chat with the selection attached and paste this prompt with the code field left as "the selected code". In Claude Code, run it on a file path and ask it to write the tests to the test directory before editing. For larger modules, run the Behaviour step alone first and confirm it before allowing the rewrite. If you do want the bugs fixed, move them from "problems" to an explicit instruction: "fix bug 3 and update the tests accordingly".
How to customize: in Cursor, select the function, open the chat with the selection attached and paste this prompt with the code field left as "the selected code". In Claude Code, run it on a file path and ask it to write the tests to the test directory before editing. For larger modules, run the Behaviour step alone first and confirm it before allowing the rewrite. If you do want the bugs fixed, move them from "problems" to an explicit instruction: "fix bug 3 and update the tests accordingly".
Comments (0)
Sign in to comment.
No comments yet.