Refactor Legacy Code Safely: Characterization Tests and an AI Refactoring Agent
Hands-on lab · IDE in your browser

Refactor Legacy Code Safely: Characterization Tests and an AI Refactoring Agent

Build an agent that refactors messy legacy code without changing its behaviour: pin the current behaviour with a characterization suite, build a regression net that runs any candidate against the golden results, prompt a model to clean the code up while preserving every quirk, wrap it in a loop that feeds regressions back and fails safe, and enforce the rule that a refactor which changes a single output is rejected even if it looks more correct.

Time
55 min
Checked steps
5
Level
Intermediate
Setup
None
Read step 1

Hands-on labs require Pro · $29.99/mo · cancel anytime

The job

You inherit total(), an order-total function with a member discount, two coupons, and a quirk everyone forgot about: one coupon only applies once the subtotal passes 100. It needs to be readable, but customers already depend on exactly what it does. You build an agent that pins the current behaviour, lets a model clean the code up, and refuses any rewrite that changes a single output.

5 steps, each checked when you finish it

A check runs your work at the end of every step. Hints and the full solution are there if you get stuck.

  1. 1

    Pin the behavior

    You have been handed total(items, member, coupon) in harness.LEGACY: an order-total function with a member discount, two coupons, and years of accumulated mess.

  2. 2

    The safety net

    With the behaviour pinned, you can let anyone (or any model) rewrite the code and instantly tell whether they changed it.

  3. 3

    The refactor agent

    Now bring in the model.

  4. 4

    Refactor with a net

    Put the pieces together into the loop a careful engineer runs by hand: pin the behaviour, refactor, check against the golden set, and if anything changed, hand the regressions back and try again.

  5. 5

    The tests are the contract

    One rule makes refactoring safe, and it is worth stating on its own: a refactor is only accepted if it reproduces the original behaviour on every input.

Step 1 as it appears in the lab

The lab’s own text. The hint and the solution stay inside the lab.

Step 1: Pin the behavior

You have been handed total(items, member, coupon) in harness.LEGACY: an order-total function with a member discount, two coupons, and years of accumulated mess. Your job is to make it readable without changing what it does. Before you touch a line, pin down exactly what it does now, quirks and all, because that behaviour is the contract your customers already depend on.

This is a characterization test: you record what the code does today, whatever it was meant to do.

Write two functions in agent.py:

  • extract_code(reply): pull code out of a model reply, taking the first ``` fenced block (dropping an optional python tag) or the whole reply when there is no fence. You will need it in Step 3.
  • characterize(src, fn, inputs): run src on every input with harness.run_calls and return a list of {"input": args, "result": record}. This golden set is the pinned behaviour.

Run it: watch that SAVE20 only takes money off once the subtotal is over 100. That is a quirk, and it is now part of the contract.

agent.py, the file you edit43 lines
"""Your refactoring agent. It pins the legacy behaviour with a characterization suite, asks a model to
clean the code up, and rejects any refactor that changes a single output. The tests are the contract:
a refactor that changes behaviour, even to something 'more correct', is not a refactor, it is a bug."""
import harness


def extract_code(reply):
    """Pull the code out of a model reply: the first ``` fenced block (dropping an optional python tag),
    or the whole reply stripped when there is no fence."""
    # TODO (Step 1): first ``` fenced block (drop a leading 'python'), else the whole reply stripped.
    raise NotImplementedError("Step 1: write extract_code()")


def characterize(src, fn, inputs):
    """Pin the current behaviour: run src on every input and record what it returns. This golden set is the
    contract the refactor must preserve, quirks included."""
    # TODO (Step 1): run src on each input via harness.run_calls; return [{input, result}] - the pinned golden behaviour.
    raise NotImplementedError("Step 1: write characterize()")


def find_regressions(candidate_src, fn, golden):
    """Run a candidate on the golden inputs and return every input whose result differs from the pinned one.
    An empty list means behaviour is preserved."""
    raise NotImplementedError("find_regressions() arrives in Step 2")


def refactor(src, ask=harness.ask, feedback=""):
    """Ask the model to refactor src for readability without changing behaviour. If a previous attempt
    changed behaviour, pass the regressions back so it can correct them. Return the extracted code."""
    raise NotImplementedError("refactor() arrives in Step 3")


def safe_refactor(src, fn, ask=harness.ask, inputs=None, max_iters=3):
    """Characterize the legacy code once, then refactor and check against the golden set, feeding any
    regressions back and retrying until the refactor is behaviour-preserving or the budget runs out.
    Returns {"code", "preserved", "iters", "regressions", "history"}."""
    raise NotImplementedError("safe_refactor() arrives in Step 4")


def accept_refactor(original_src, candidate_src, fn, inputs=None):
    """The gate: accept a candidate only if it reproduces the original's behaviour on every input.
    Returns {"accepted", "regressions"}."""
    raise NotImplementedError("accept_refactor() arrives in Step 5")
Provided for you:harness.pytry_it.py

Frequently asked questions

What is a characterization test?

A characterization test records the current behaviour of existing code by running it on a set of inputs and saving the outputs as the expected results, quirks and all. It does not assert what the code should do; it pins what it does, so a later refactor that changes any output is caught.

How do you refactor legacy code without breaking it?

Pin the current behaviour with characterization tests, make small changes, and re-run the tests after every change. A refactor is only safe if every pinned output stays identical; if a test changes, you have altered behaviour as well as structure.

Can an AI model refactor code safely?

Yes, if it works inside a safety net. A model told to clean code up will often simplify away a quirk it assumes is a bug, so the agent must run the model's output against a characterization suite and reject any refactor that changes behaviour, feeding the regressions back for another attempt.

Safe refactoring with characterization tests and an AI agent

Refactoring means changing the shape of code without changing what it does, and the only way to know you succeeded is to pin the behaviour first. Characterization tests record what legacy code actually does, quirks included, so any change shows up immediately. In this lab you build an AI refactoring agent around that safety net: it characterizes a messy function, asks a model to clean it up, checks the result against the golden behaviour, feeds any regressions back, and enforces the rule that a cleaner-looking rewrite which changes an output is a behaviour change and gets rejected.