A refactor must not change behaviour
Refactoring with an AI assistant means asking it to improve how code is written without changing what it does. It matters because an assistant can rewrite a whole file in seconds, and a rewrite that changes behaviour is a bug you never asked for.
The skill is proving that nothing changed.
By the end of this lesson
Note
I can ask for a refactor and prove the behaviour did not change.
Key words
| Word | What it means |
|---|---|
| Refactor | to change how code is written, not what it does |
| Behaviour | what the code gives back for each input |
| Baseline | the outputs you record before a change |
| Small step | one change you can check on its own |
| Regression | old behaviour that breaks after a change |
The cells students fill in are left blank in this preview.
Record what the code does first
Before you change anything, write down what the code gives for a set of inputs. That list is the baseline. It can be your tests, or just a printout.
Choose inputs where code tends to slip: zero, a small value, a large one. A baseline of easy inputs lets a change that breaks the hard ones through.
One possible request
Tip
In label.js the money formatting is written out twice. Move it into one function called money. Change nothing else, and do not change what label returns for any input. Show me the diff before you save.
Ask for one kind of change
Ask for one small step and check it before the next. Moving code into a function is one step. Renaming is another. Changing a rule is not a refactor at all.
A big rewrite gives you a big diff to read. If it breaks, you cannot tell which part did it. The next Runs write a function, a clean move of its money code, and a second attempt.
Write the original function
docker exec -w /home/node/project agent sh -c "mkdir -p refactor-demo && printf '%s\n' '// Before: the money formatting is written out twice.' 'function label(item) {' ' const c = item.pence * item.qty;' ' if (item.qty === 1) {' ' return item.name + \" \" + Math.floor(c / 100) + \".\" + (c % 100 < 10 ? \"0\" : \"\") + (c % 100);' ' }' ' return item.qty + \" x \" + item.name + \" \" + Math.floor(c / 100) + \".\" + (c % 100 < 10 ? \"0\" : \"\") + (c % 100);' '}' 'module.exports = label;' > refactor-demo/before.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the first rewrite
docker exec -w /home/node/project agent sh -c "mkdir -p refactor-demo && printf '%s\n' '// After: the money formatting is written once.' 'function money(p) {' ' return Math.floor(p / 100) + \".\" + (p % 100 < 10 ? \"0\" : \"\") + (p % 100);' '}' 'function label(item) {' ' const text = item.name + \" \" + money(item.pence * item.qty);' ' return item.qty === 1 ? text : item.qty + \" x \" + text;' '}' 'module.exports = label;' > refactor-demo/after.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the second rewrite
docker exec -w /home/node/project agent sh -c "mkdir -p refactor-demo && printf '%s\n' '// After, a second attempt: also written once.' 'function money(p) {' ' return Math.floor(p / 100) + \".\" + (p % 100);' '}' 'function label(item) {' ' const text = item.name + \" \" + money(item.pence * item.qty);' ' return item.qty === 1 ? text : item.qty + \" x \" + text;' '}' 'module.exports = label;' > refactor-demo/after-bad.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the comparison, part 1
docker exec -w /home/node/project agent sh -c "mkdir -p refactor-demo && printf '%s\n' 'const cases = [' ' { name: \"apple\", qty: 1, pence: 35 },' ' { name: \"crisps\", qty: 3, pence: 60 },' ' { name: \"sweet\", qty: 1, pence: 5 },' ' { name: \"bag\", qty: 2, pence: 1000 },' '];' 'const before = require(\"./before\");' > refactor-demo/check.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Write the comparison, part 2
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'for (const file of [\"after\", \"after-bad\"]) {' ' const after = require(\"./\" + file);' ' let same = 0;' ' for (const item of cases) {' ' if (after(item) === before(item)) {' ' same = same + 1;' ' } else {' ' console.log(file + \": \" + before(item) + \" became \" + after(item));' ' }' ' }' ' console.log(file + \": same on \" + same + \" of \" + cases.length);' '}' >> refactor-demo/check.js"Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
Compare both rewrites with the original
docker exec -w /home/node/project agent node refactor-demo/check.jsShown here, not run. Create a free account to press Run and type it into your own lab’s terminal.
What the comparison printed
| Rewrite | What it printed |
|---|---|
| after | same on 4 of 4 |
| after-bad | sweet 0.05 became sweet 0.5 |
| after-bad | 2 x bag 20.00 became 2 x bag 20.0 |
| after-bad | same on 2 of 4 |
The cells students fill in are left blank in this preview.
Read the diff for changed lines
A clean refactor diff mostly shows lines that moved: the same text removed in one place and added in another. Look for lines that changed instead.
In the second rewrite, the removed money line and the added one differ by a missing zero. That small difference is the whole bug.
A common mistake
Warning
Many people think that a cleaner refactor behaves the same. The second rewrite looked as tidy as the first and lost the zero in 0.05. Only running both on the same inputs shows it, including the small and zero ones.
Try it on a change of your own
The lab's own job is the shop's fault, and this lesson leaves it alone. It is a good time to try the ideas on a different change.
Pick a function you wrote, such as a price formatter. Record its outputs, ask the assistant for one refactor, run the same inputs again and read the diff for lines that changed.