Cursor Recipe

Refactor-with-tests recipe for Cursor

A Cursor recipe that writes a characterization test before each refactor step.

LLM Mart · 19 points · 573 views 384 listing impressions

#code-review

What vetted this — trust report


Refactor with a safety net

A recipe for changing structure without changing behaviour. The test comes first, so you always have proof you didn't break anything — which matters more with an agent than without one, because an agent will confidently "improve" behaviour you needed to keep.

A characterization test is not a correctness test. It asserts what the code currently does, bugs included. That's the point: during a refactor, the existing bugs are part of the contract.

Recipe

1. Pin current behaviour

In Cursor, with the file open, Cmd/Ctrl+K:

Write a characterization test for {function}. Assert the current output for typical, edge, and error inputs. Do not fix bugs — capture behaviour as-is, including anything that looks wrong. Where the current behaviour is surprising, add a comment saying so rather than changing the assertion.

Run it. It must pass against the unchanged code. If it fails, the test is wrong about current behaviour — fix the test, never the code, at this stage.

2. Commit the test on its own

A separate commit, before any production change. Now you have a green baseline you can git checkout back to, and a diff later that clearly separates "what I pinned" from "what I changed".

3. Refactor in one small step

One kind of change at a time — rename, or extract, or inline, or move. Not three at once.

Refactor {X} to {Y}. Keep the public behaviour identical. Do not change any other file. Do not modify the tests.

That last sentence is not optional. Without it, agents resolve a failing test by editing the test, which silently destroys the entire safety net.

4. Run the test

  • Green → continue.
  • Red → the refactor changed behaviour. Revert or reconcile before moving on. Never edit the test to make it pass.

5. Repeat 3–4 in small increments

Commit after each green step. The discipline pays off exactly when something breaks: a red bar after a one-change step points at that one change.

6. Only then, change behaviour

Once the structure has settled, if you want a behaviour change: change the test first, so the diff shows intent, then change the code. A reviewer reading that commit sees "we deliberately changed this" instead of "something moved."

Why it works

The characterization test converts "I think this still works" into "the suite proves it does". Small steps make a red bar diagnostic instead of a mystery. And the separate commits mean git bisect is useful rather than pointing at a 500-line "refactor" blob.

Cursor specifics

  • Use Cmd/Ctrl+K inline edit, not the agent, for steps 3–4. You want a bounded, reviewable diff, and the agent's willingness to touch adjacent files is a liability here.
  • @file the test file explicitly when refactoring so it has the contract in view, then tell it not to modify it.
  • Put the rule in .cursor/rules/ so you stop retyping it: "During a refactor, never modify an existing test to make it pass. Report the failure instead."
  • Turn off auto-run of terminal commands for this workflow. You want to see each test run.

Watch out for

  • Tests that assert on incidental details — log strings, dictionary ordering, exact whitespace, timestamps. They break on harmless refactors and train you to ignore red. Assert on behaviour, not implementation.
  • Untestable seams. If you can't pin the behaviour because the function reaches into a database, a clock, or the network, extract a smaller unit first. The extraction is itself a refactor — pin what you can first.
  • Snapshot tests as characterization tests. Tempting, and they do pin behaviour, but a failing snapshot tells you something changed, not what. Fine as a backstop, bad as your only signal.
  • The agent "helpfully" fixing the bug you were preserving. This is the most common way the recipe fails. Say so explicitly, every time.

When to skip this

If the code has real tests already and they cover the behaviour you care about, you don't need characterization tests — you need to run the ones you have. And if the function is three lines with no callers, just change it.

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related