Skip to content
Runtime

How to give a coding agent fast Vitest feedback in a sandbox

Keep one sandbox per task. Run vitest related on the files the agent changed, and return the JSON report to the model.

On Runtime, a sandbox pauses while the model is thinking, and it costs almost nothing during that time. A sandbox with no activity pauses after idlePauseSeconds, which defaults to 300. It keeps its files, memory and processes. While it runs and waits, a 2 vCPU, 4 GiB sandbox costs $0.03125 an hour. Vitest 5.0.5, the current release on 25 October 2026, needs Node.js 22.12 or 24, and the image has Node.js 24.21.0.

The loop

On each turn, the agent writes files. The host runs only the tests that import those files, and returns a short summary of the failures. getOrCreate returns the same sandbox on every turn, so node_modules installs once per task:

TypeScriptimport { Sandbox } from "withruntime";type Report = {  numFailedTests: number;  numTotalTests: number;  testResults: {    name: string;    assertionResults: { fullName: string; status: string; failureMessages: string[] | null }[];  }[];};export async function testTurn(taskId: string, changed: Record<string, string>) {  const sbx = await Sandbox.getOrCreate(`task-${taskId}`, { idlePauseSeconds: 300 });  if (!sbx.info.reused) {    await sbx.files.upload("./repo", "/workspace/repo");    await sbx.exec("cd repo && npm ci --no-fund --no-audit", { check: true, timeoutMs: 600_000 });  }  for (const [path, content] of Object.entries(changed))    await sbx.files.write(`/workspace/repo/${path}`, content);  const files = Object.keys(changed).join(" ");  await sbx.exec(    `cd repo && npx vitest related ${files} --run --reporter=json --outputFile=/workspace/vitest.json`,    { timeoutMs: 300_000 },  );  const report: Report = JSON.parse(await sbx.files.readText("/workspace/vitest.json"));  const failures = report.testResults.flatMap((file) =>    file.assertionResults      .filter((test) => test.status === "failed")      .map((test) => `${file.name} > ${test.fullName}\n${(test.failureMessages ?? []).join("\n")}`),  );  return { passed: report.numTotalTests - report.numFailedTests, failures };}
Pythonimport jsonfrom withruntime import Sandboxdef test_turn(task_id: str, changed: dict[str, str]) -> dict:    sbx = Sandbox.get_or_create(f"task-{task_id}", idle_pause_seconds=300)    if not sbx.info.get("reused"):        sbx.files.upload("./repo", "/workspace/repo")        sbx.exec("cd repo && npm ci --no-fund --no-audit", check=True, timeout_ms=600_000)    for path, content in changed.items():        sbx.files.write(f"/workspace/repo/{path}", content)    files = " ".join(changed)    sbx.exec(f"cd repo && npx vitest related {files} --run --reporter=json --outputFile=/workspace/vitest.json",             timeout_ms=300_000)    report = json.loads(sbx.files.read_text("/workspace/vitest.json"))    failures = [        f"{result['name']} > {test['fullName']}\n" + "\n".join(test.get("failureMessages") or [])        for result in report["testResults"]        for test in result["assertionResults"]        if test["status"] == "failed"    ]    return {"passed": report["numTotalTests"] - report["numFailedTests"], "failures": failures}

The agent chooses the file names. Pass only paths you have checked, or build the command as an array (["npx", "vitest", "related", ...files]). An array runs with no shell.

Why a JSON report and not the console

Vitest's console output is written for people. It includes colors, progress lines and diffs. A model needs only the failing test's name and its assertion message. The json reporter returns that structure. Jest's JSON output uses the same structure:

Field What it holds
success true when every test passed
numTotalTests, numFailedTests Counts across the run
testResults[].name The test file
testResults[].assertionResults[].fullName The test's full name
assertionResults[].status passed, failed, skipped and so on
assertionResults[].failureMessages The assertion error and its stack

Send failures to the model as the next message. On a large project, this is a few hundred tokens instead of the full console log.

Which Vitest command for which turn

Situation Command
The agent edited a few source files vitest related <files> --run
The agent committed its work with git vitest run --changed HEAD~1
Before declaring the task done vitest run
A long suite split over sandboxes vitest run --shard=1/3, --shard=2/3, ...
Stop at the first failures add --bail 1

Running vitest with no command starts watch mode. When standard input is not a terminal, as with exec, it runs once instead. Pass --run so the behavior does not depend on the terminal.

Start every task with Vitest installed

For an agent that starts new projects instead of editing a repo, build a template into a custom image. Recipe commands run as root. At the end, change the owner of the folder to the sandbox user (uid 1000):

TypeScriptimport { Runtime } from "withruntime";const runtime = new Runtime();await runtime.images.build({  name: "ts-vitest",  recipe: {    commands: [      "mkdir -p app && cd app && npm init -y && npm install -D --no-fund --no-audit vitest@5.0.5 typescript",      "chown -R 1000:1000 /workspace/app",    ],  },});await using sbx = await runtime.sandboxes.create({ image: "ts-vitest" });await sbx.files.write(  "/workspace/app/sum.ts",  "export const sum = (a: number, b: number) => a + b;\n",);await sbx.files.write(  "/workspace/app/sum.test.ts",  'import { expect, test } from "vitest";\nimport { sum } from "./sum";\ntest("adds", () => expect(sum(2, 3)).toBe(5));\n',);const run = await sbx.exec("cd app && npx vitest run", { timeoutMs: 120_000 });console.log(run.exitCode, run.stdout);

A stored image is charged at $0.08 per GB per 30-day month on its accounted storage. Building an image is free. An account without credit stores its first three images free.

Between turns

  • Pause: idlePauseSeconds: 300 pauses the sandbox after five minutes with no activity. A test run in progress keeps it awake. Compute billing stops at the pause. The next exec or file call wakes it.
  • Lease: A sandbox runs for its timeoutSeconds, then pauses by default. Call extend to add time to a long task.
  • Clean up: Call stop() when the task ends. The name is then free for the next task.

Sources

Checked 25 October 2026.

A stored image is charged for its whole file (image storage pricing).

Facts on this page were checked on 25 October 2026.

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