An agent changes legacy code quickly. It reads the module, understands what it thinks the code is for, and rewrites it cleanly. The new version is shorter, better named and easier to test. It also behaves slightly differently, and nobody can say how, because nobody wrote down what the old version did.
That is the specific risk of putting agents on old systems. The speed is welcome. What it removes is the pause in which a careful engineer would have asked what depends on this.
What legacy code carries
Old systems carry behaviour nobody wrote down. Some of it is accidental. Some of it is relied on by people who no longer remember they rely on it. The 2 kinds look identical in the code.
- A rounding rule that a downstream finance report depends on
- An error path that returns an empty list, which another team treats as "no results" rather than a failure
- A date format in an export that a partner parses with a regular expression
- A bug that a large client built a workaround for, so fixing it breaks the workaround
None of these appear in the specification, because the specification describes what the system was meant to do. The system's users depend on what it does.
The specification describes intent. Users depend on behaviour. Legacy work goes wrong in the space between them.
What characterisation tests are
A characterisation test records what the system does today, correct or not. It does not ask whether the output is right. It asks whether the output is the same as yesterday's. That makes it the one kind of test that can be written for code nobody fully understands.
The technique is old and simple. Feed the system realistic inputs, capture its outputs, and store them as the expected result. Run the same inputs after a change and compare. A difference is not automatically a bug. It is a question that someone with domain knowledge has to answer before the change goes further.
Recording before the first edit
The order is the whole discipline. Characterisation comes before the first change, because afterwards there is nothing left to record. Once the agent has rewritten the module, the old behaviour exists only in production, and the only way to rediscover it is to wait for someone to complain.
Inputs come from production traffic where that is possible, with personal data removed. Real inputs reach the strange branches that synthetic ones miss: the customer record with no postcode, the order placed across midnight on a daylight-saving change, the invoice with a zero-quantity line. Those branches are where the undocumented behaviour lives.
Naming, then judging
Recording is mechanical. Judging is not. Each recorded behaviour gets a label and an owner.
- Intended: the system should keep doing this
- Accidental and harmless: it can change, and nobody will notice
- Accidental and relied on: it must stay until whoever relies on it has moved
- Unknown: someone has to find out before the area is touched
The unknown pile is usually the largest at first, and that is useful information. It shows exactly where the team's understanding of its own system runs out, which is where a careless change would have done the most damage.
This step needs people who know the domain. An agent can propose labels and find the callers of a function, but it cannot know that the finance team's quarter-end report reads a column the code treats as internal. Someone has to ask.
A safety net for the agent
With the behaviour recorded and labelled, the agent can work freely inside the net. It refactors, renames and restructures, and the characterisation suite runs on each change. A diff in recorded output stops the change and shows which behaviour moved.
That turns legacy work from a slow, cautious process into a fast one with a clear rule. The agent is not asked to be careful, which it cannot be about things it does not know. It is asked to keep the recorded behaviour the same, and the suite checks that it did.
When a change is meant to alter behaviour, the change says so. The relevant recorded outputs are updated in the same pull request, with a note on who approved the new behaviour and why. The record of what the system does stays true after each change.
Where the agent helps
Agents are good at most of the mechanical work in characterisation, which is why the technique has become practical on systems where it used to be too slow.
- Generating input sets that reach each branch of a module, starting from real traffic and filling the gaps
- Finding the callers of each function, including the ones in other repositories and in scheduled jobs
- Proposing a label for each recorded behaviour, with the evidence for it, for a person to accept or correct
- Running the comparison on each change and explaining which inputs moved and why
What the agent cannot do is decide which behaviour is intended. It can show that a rounding rule exists and who calls the function. Whether the finance team depends on it is a question for the finance team.
Where to characterise first
Characterising a whole legacy system up front is rarely worth it. Characterise where change is planned. The roadmap already says which modules will be touched in the next quarter. Those modules get characterised first, in the order they will be changed.
Two signals help pick the order within that list. Modules with many callers carry more hidden dependents. Modules with a history of production incidents carry behaviour that has already surprised someone once.
The module nobody wants to touch
Pick the module your team least wants to touch. It is usually the one most in need of change and least understood. Spend a week recording its behaviour on real inputs before anyone edits it, then go through the recordings with the people who use its outputs.
The first session normally finds at least one behaviour that someone depends on and nobody had written down. Finding it there costs an afternoon. Finding it after the rewrite costs an incident, and usually a conversation with a client about why their numbers changed.
