A 30-minute test suite in five minutes
Cutting every test off from the real world let Mullet’s 17,700 tests run in parallel, and flushed out a real Kimi call and a fixture deleting a real file.
Mullet’s test suite used to take about 30 minutes. Now it is about 17,700 tests, run on 8 workers with pytest-xdist, in about 5 minutes.
The speed-up is not the interesting part. Parallel runs only work when tests cannot affect each other or the machine they run on. Making that true was the real work, and it found things I did not know were wrong.
Why it was slow, and why that mattered
Thirty minutes means you do not run the suite before committing. You run the file you changed and hope. Mullet 2’s gates compare test results against a baseline, so a suite you cannot run quickly is a suite you cannot lean on. Five minutes is short enough to run before a commit that touches several modules.
My working rule is still to run only the files I changed while I work, and the full suite before committing across modules. The command is:
python3 -m pytest tests -q -n 8 --timeout 300
Cutting tests off from the real world
Parallel tests need isolation, and isolation has four parts here. Every test gets:
- No network beyond localhost. A guard on
socket.connectfails any test that tries to reach out. - Provider keys removed from the environment.
- A throwaway
HOME. - Mock providers. Tests that need a real model are marked
liveand excluded by default.
The guard is blunt on purpose. It does not try to guess which calls are harmless. A test that connects to anything but localhost fails and tells you where.
The throwaway HOME matters as much as the network guard. Anything that writes to ~ writes to a temporary directory that is deleted afterwards. If a test used to write to my real home, it now cannot.
What the isolation found
I expected a few failures. I did not expect these.
A test had been calling the real Kimi API. A test that makes a real call passes when the real service answers. With the network guard and no keys, it failed.
An old fixture deleted my real learned-constraints file. Under a throwaway HOME, the path it deleted no longer resolved to my file. I might never have found that by reading the suite, because a fixture that deletes something is easy to skim past.
Some tests were writing to my real HUD, the status bar Mullet draws in Claude Code. That explains something that had puzzled me for a while: I kept seeing “Kimi agents” in my status bar when nothing was delegating. The tests had been drawing them.
All three were the same fault. The tests were reaching out of their own sandbox, and nothing had stopped them. They only showed up when something did.
The same idea in the product
This is the same principle as the sandbox around the worker, described in Sandboxing an AI coding worker. Code you did not look at closely should not be able to reach things you did not intend. For the tests, the “worker” was mine.
It also fits the gates in Gates, not a model’s word. A G5 comparison against a baseline is only worth anything if the baseline is repeatable. A suite that depends on the network, the clock or the contents of my home directory is not.
What I did not do
I also have not tried to make the suite faster than about 5 minutes. It is fast enough for now.
What changed in how I work
Before, a green full run was an event. Now it is a step I take before a commit that touches several modules. The run is cut off from the network, the keys and my home directory, so a pass on my laptop means the same as a pass anywhere else. That was not true when some tests quietly used my real files and my real provider.
Doing the same to yours
If you have a suite you do not trust to run in parallel, the order I would try is:
- Put a guard on
socket.connectthat fails anything outside localhost. - Strip provider keys and credentials from the test environment.
- Point
HOMEat a temporary directory. - Run it. Read every failure, because each one is a test reaching somewhere it should not.
- Then add
-nworkers.
Do the fourth step before the fifth. Parallelism will make the failures harder to read.
The code is at https://github.com/tomgould/mullet-public.
More on: Mullet