Legacy Application Testing
Introduces characterization tests that document existing behavior before risky legacy changes.
Scenario
You maintain a legacy Node.js application with limited test coverage.
Some modules have good tests.
Others have:
- no tests;
- difficult-to-isolate dependencies;
- global state;
- direct database access;
- older patterns that are expensive to refactor.
The long-term goal is better test coverage, but requiring a large testing rewrite before every change would make normal maintenance impractical.
You want coding agents to improve safety incrementally.
Repository Structure
legacy-app/
├── src/
│ ├── modern/
│ │ ├── services/
│ │ └── repositories/
│ └── legacy/
│ ├── reports.js
│ ├── billing.js
│ └── notifications.js
├── tests/
│ ├── modern/
│ └── legacy/
├── package.json
└── AGENTS.md
AGENTS.md
# Project Instructions
## Legacy Code
- Preserve existing behavior unless the task explicitly changes it.
- Avoid unrelated refactors while making targeted fixes.
- Follow the local style of the module unless modernization is part of the task.
## Testing Existing Behavior
Before changing poorly tested legacy code:
- inspect existing tests;
- identify the behavior affected by the change;
- add characterization or regression coverage when practical.
When fixing a reproducible bug:
- add a regression test at the safest practical level;
- confirm the test reproduces the failure when possible;
- keep the test after the fix.
## Refactoring
- Do not rewrite a legacy module solely to make it easier to test.
- Small refactors are acceptable when necessary to introduce a safe test seam.
- Separate large modernization work from unrelated feature or bug-fix tasks.
## Validation
- Run existing tests for the affected area.
- Run newly added tests.
- Run broader tests when shared legacy behavior changes.
- Report important areas that could not be tested reliably.
What This Does
This gives the agent a pragmatic strategy for working with imperfect code.
The objective is not:
Make the legacy application perfect before touching it.
Instead:
Understand existing behavior
↓
Protect the behavior being changed
↓
Make the smallest safe change
↓
Improve coverage where practical
For example, suppose a legacy reporting function has no tests and a bug needs fixing.
A useful first step may be a characterization test that captures its current behavior around the affected case.
Then the bug can be fixed with evidence that unrelated behavior has not accidentally changed.
What This Does NOT Do
The instructions do not require maintaining bad architecture forever.
They simply separate:
targeted maintenance
from:
intentional modernization
This is important because an agent may see old code and decide to rewrite it using modern patterns while solving a small bug.
That can greatly increase the change's risk.
The file also does not promise that every legacy behavior can be tested easily.
Sometimes the repository may require manual verification or broader integration testing until the architecture improves.
Why These Instructions Matter
Legacy systems often contain hidden behavior.
Consider a function like:
function generateReport(account) {
// 300 lines
// database calls
// formatting
// business rules
// side effects
}
An agent might reasonably want to split it into many services before fixing one formatting bug.
But the refactor could alter behavior that has no tests.
A safer strategy may be:
- reproduce the formatting bug;
- add a test around that behavior;
- make a small change;
- verify the test;
- defer the larger refactor.
This reduces the number of assumptions introduced at once.
Key Decisions
Characterization tests can create a safety net
A characterization test captures what the system currently does.
It does not necessarily claim that every existing behavior is ideal.
It creates evidence that a future change altered something.
Improve incrementally
A bug fix can leave the area slightly safer than before without requiring complete modernization.
Small test seams are acceptable
Sometimes a small refactor is necessary to make behavior testable.
For example, extracting an external call behind a function boundary may allow deterministic testing.
The important point is to keep the refactor proportional to the task.
Report testing limitations
If meaningful behavior cannot be automatically verified, the agent should say so.
For example:
Automated tests cover the calculation change, but the legacy email
integration has no isolated test environment and was not executed.
That is more useful than implying complete validation.
Don't silently rewrite legacy code
Old code may look strange because of constraints that are not obvious from one file.
Large changes should be intentional and separately reviewable.
When to Use This Pattern
Use this pattern when:
- test coverage is incomplete;
- legacy modules contain hidden behavior;
- large refactors would increase task risk;
- characterization tests can provide incremental safety;
- modernization needs to happen gradually.
The goal is not to preserve legacy code forever.
The goal is to make today's change safely while improving tomorrow's ability to change it again.