You have a messy repo. No tests. An AI suggests a sweeping refactor. You say no.
Big changes without a safety net are disasters. The safest unit is a single function. Lock it. Change it. Verify it. Commit it. Then repeat.
Here is a concrete workflow that uses a free model to write the lock and a free server to run it.
Step 1: Pick One Messy Function
Choose a function with clear inputs and outputs. Avoid functions with network calls or hidden state. A pure calculation is perfect.
// pricing.js — no tests, untouchable legacy
export function calculateTotal(cart, taxRate) {
let subtotal = 0;
for (const item of cart) {
subtotal += item.price * item.qty;
}
let tax = subtotal * taxRate;
if (cart.length > 5) {
tax = tax * 0.9; // loyalty discount nobody remembers adding
}
return { subtotal, tax, total: subtotal + tax };
}
Do not refactor yet. First, record what it does today.
Step 2: Generate a Characterization Test
Ask an AI to write a test that freezes current behavior. I used MonkeyCode's free model for this. Disclosure: This article was prepared as part of MonkeyCode's product outreach.
Give the model the function and a simple prompt:
Write a node:test test that calls calculateTotal with a cart of 2 items and taxRate 0.1. The test should only record the current output, not judge it.
The result:
import test from 'node:test';
import assert from 'node:assert/strict';
import { calculateTotal } from './pricing.js';
test('calculateTotal records current behavior', () => {
const cart = [
{ price: 10, qty: 2 },
{ price: 5, qty: 1 },
];
const result = calculateTotal(cart, 0.1);
assert.deepEqual(result, { subtotal: 25, tax: 2.5, total: 27.5 });
});
Read that test carefully. Free models can invent outputs. If the expected value looks wrong, run the function with known numbers and confirm before trusting it.
Step 3: Run the Lock on a Free Server
You need an environment where the test runs in isolation. Configure a checkout, install dependencies, and run the suite.
MonkeyCode's free server option gives you a disposable environment for this. No CI setup. No shared runner. Just a clean slate.
npm test
The test passes. Now you have a baseline.
Step 4: Make the Smallest Change
The goal is one commit that does not change behavior. Extract the tax logic into a helper function. Do not touch anything else.
export function calculateTotal(cart, taxRate) {
const subtotal = calcSubtotal(cart);
const tax = calcTax(subtotal, cart.length, taxRate);
return { subtotal, tax, total: subtotal + tax };
}
function calcSubtotal(cart) {
return cart.reduce((acc, item) => acc + item.price * item.qty, 0);
}
function calcTax(subtotal, itemCount, taxRate) {
let tax = subtotal * taxRate;
if (itemCount > 5) {
tax *= 0.9;
}
return tax;
}
Run the same test again.
It still passes. The refactor did not change behavior.
Step 5: Commit and Repeat
Commit the test and the refactor together. One function. One test. One commit.
git add pricing.js pricing.test.js
git commit -m "Extract tax helpers from calculateTotal"
Then pick the next function. Repeat the cycle.
Why This Works
The scope is tiny. A failure in a one-function change is easy to locate. The characterization test catches regressions immediately. The free tools remove friction: no local VM, no paid model credits.
This is not a rewrite strategy. It is a patience strategy. You turn a rat's nest into a sequence of verified microsteps.
Limitations
Free models have limits. They may generate tests that match a hallucinated version of the code. Always compare test output to actual runtime behavior.
Free server options also have resource limits. Long-running suites or heavy parallel tests may not fit. Keep the work small.
Characterization tests record current behavior. They do not validate that behavior. If the logic is buggy, your lock freezes the bug.
Who Should Not Use This
Do not use this workflow if you need a full architectural rewrite in one sprint. It will feel too slow.
Do not use it if your repo has no clear function boundaries. You first need to identify the functions.
And if your code cannot be copied to an external environment for testing, verify where the free server runs before touching it.
Start With One Function
Open the messiest file. Find the function that scares you most. Write one characterization test. Run it on the free server. Make the smallest change. Commit.
That is the entire method. Small steps are boring. They are also the only refactors that ship safely.
Top comments (0)