Skip to content
Runtime

How to generate unit tests with an LLM and keep only the ones that pass

Run each model-written test in a sandbox three times. Keep the test only if it passes every time and raises coverage.

On Runtime, each candidate test runs on a fresh copy of a machine that already has the project installed. A fork copies files, memory and running processes into 1 to 100 new sandboxes in one call. Runtime bills measured CPU at $0.025 per vCPU-hour.

Why generated tests need a gate

A model asked for tests produces files that look right. Some fail on the first run because they call a function wrongly. Some pass by asserting nothing useful. Some pass once and fail the next time, because they depend on time, order or randomness. Some cover code that the suite already covered. Merging them unchecked adds noise to the suite and teaches people to ignore failures.

The gate is mechanical, and a sandbox is where it runs:

  1. The test file must pass.
  2. It must pass again, three runs in a row, to rule out flakiness.
  3. Coverage must rise: the file has to execute lines that no existing test reaches.
  4. The rest of the suite must still pass with the new file added.

Running model-written tests on a laptop or CI runner means running model-written code there. In a sandbox, the code runs in a Firecracker microVM with its own kernel. Once the dependencies are installed, the sandbox has no internet access.

Measure the baseline

coverage.py runs a test runner under measurement when you use coverage run -m. coverage json writes the results to coverage.json (coverage.py's documentation, read 26 October 2026). Install the project once, then record what is covered:

TypeScriptimport { Sandbox } from "withruntime";await using base = await Sandbox.create({ diskMiB: 8192, timeoutSeconds: 3600 });await base.exec("git clone --depth 1 https://github.com/your-org/your-lib.git repo", {  check: true,  timeoutMs: 300_000,});await base.exec("cd repo && pip install -e . pytest coverage", { check: true, timeoutMs: 600_000 });await base.exec(  "cd repo && coverage run -m pytest -q && coverage json -o /workspace/baseline.json",  {    timeoutMs: 900_000,  },);await base.network.set({ internet: false }); // nothing the model writes can reach outconst baseline = JSON.parse(await base.files.readText("/workspace/baseline.json"));console.log("baseline:", baseline.totals.percent_covered);

Try each candidate on its own copy

Fork the prepared machine. Write one candidate into each copy, and apply the gate. Each copy starts with the project installed and the baseline on disk. No copy can disturb another.

TypeScriptimport { Sandbox } from "withruntime";const candidates: string[] = []; // test files the model wrote, up to 10const base = await Sandbox.connect("your-prepared-sandbox-id");const copies = await base.fork({ count: Math.max(1, candidates.length) });const verdicts = await Promise.all(  copies.map(async (copy, i) => {    await copy.files.write("/workspace/repo/tests/test_generated.py", candidates[i] ?? "");    for (let run = 0; run < 3; run++) {      const r = await copy.exec("python3 -m pytest -q tests/test_generated.py", {        cwd: "/workspace/repo",        timeoutMs: 120_000,      });      if (r.exitCode !== 0) return { i, keep: false, why: r.stdout.slice(-1500) };    }    const full = await copy.exec(      "coverage run -m pytest -q && coverage json -o /workspace/after.json",      {        cwd: "/workspace/repo",        timeoutMs: 900_000,      },    );    if (full.exitCode !== 0) return { i, keep: false, why: "breaks the suite" };    const after = JSON.parse(await copy.files.readText("/workspace/after.json"));    const before = JSON.parse(await copy.files.readText("/workspace/baseline.json"));    return { i, keep: after.totals.covered_lines > before.totals.covered_lines };  }),);await Promise.all(copies.map((c) => c.stop()));console.log(verdicts);
Pythonimport jsonfrom withruntime import Sandboxcandidates = []  # test files the model wrote, up to 10base = Sandbox.connect("your-prepared-sandbox-id")copies = base.fork(count=max(1, len(candidates)))def judge(i, copy):    copy.files.write("/workspace/repo/tests/test_generated.py", candidates[i] if i < len(candidates) else "")    for _ in range(3):        r = copy.exec("python3 -m pytest -q tests/test_generated.py", cwd="/workspace/repo", timeout_ms=120_000)        if r.exit_code != 0:            return {"i": i, "keep": False, "why": r.stdout[-1500:]}    full = copy.exec("coverage run -m pytest -q && coverage json -o /workspace/after.json",                     cwd="/workspace/repo", timeout_ms=900_000)    if full.exit_code != 0:        return {"i": i, "keep": False, "why": "breaks the suite"}    after = json.loads(copy.files.read_text("/workspace/after.json"))    before = json.loads(copy.files.read_text("/workspace/baseline.json"))    return {"i": i, "keep": after["totals"]["covered_lines"] > before["totals"]["covered_lines"]}verdicts = [judge(i, copy) for i, copy in enumerate(copies)]for copy in copies:    copy.stop()print(verdicts)

A rejected candidate goes back to the model with its failure output. The output says either "this assertion failed" or "it passes but covers nothing new". In the second case, include the uncovered lines of the module. The missing_lines for each file in coverage.json are the most useful input for that prompt.

Why a fork and not a fresh sandbox

Installing a real project takes minutes. A fork skips it. While the fork captures it, the source pauses, then carries on. Each copy is billed as a sandbox of the same size would be. The snapshot that the fork makes for itself is deleted afterward and is not charged (snapshots and forks). For more than 100 candidates, fork again, or keep a snapshot and create copies from it with snapshot: id.

What test generation needs

Need How Runtime covers it
Running model-written tests safely A microVM with its own kernel; internet turned off after the install
An identical start for each attempt fork copies disk, memory and processes; up to 100 per call
Detecting flaky tests Repeat runs on the same copy; timeoutMs catches a test that hangs
Coverage data back in your code files.readText on coverage.json
JavaScript and TypeScript projects Node.js 24 and Bun in the default image
Leaving a sandbox running by mistake Idle sandboxes pause on their own; timeoutSeconds with onLeaseEnd: "stop" stops one at a fixed time

What it costs

Take 400 modules a month. Each gets a 12-minute session on a 2 vCPU, 4 GiB sandbox. The test runs use 300 CPU-seconds in total:

CPU
400 × 300 s / 3,600 × $0.025
$0.83
Memory
400 × 720 s / 3,600 × 4 GiB × $0.0075
$2.40
Total
$3.23

Forked copies are billed the same way, by their own running seconds and measured CPU. The figure holds however the session splits its work between copies. Rates are on the pricing page. Every account gets 100 free hours of a 1 GB machine, with no card required.

Sources

  • coverage.py: run and json commands, read 26 October 2026. The totals.covered_lines, totals.percent_covered and per-file missing_lines fields were checked by running coverage.py 7.16.1 on 25 September 2026.

Related: parallel agent exploration, CI for agent pull requests, what a sandbox fork is, agent evals and SWE-bench.

Facts on this page were checked on 26 October 2026.

Related.

100 free hoursin your first 30 days.

of a 1 GB machine

  • No credit card
  • Four sandboxes at once, 2 vCPU and 4 GiB each
  • Then prepaid credit from $10, no plan fee
Start free, no card
Start free, no card