Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

Tests as guardrails for coding agents

After this lesson: I can write a failing test first so that a coding agent's work is checked.

65 min lesson6 commandsAI coding agent lab: use opencode to explain and fix code, and check its work

Why a test comes before the agent

A guardrail test is a test you write before a coding agent starts. It fails now and must pass when the work is done, so a machine decides "done" and not the agent's own report.

It matters because an agent can report success when the work is not done. A test written first, and kept safe from the agent, is a check you can trust.

By the end of this lesson

Note

I can write a failing test first so that a coding agent's work is checked.

Key words

WordWhat it means
Guardrail testa test written first that the work must pass
Red, then greenthe test fails before the work and passes after
Cheata change that passes a test without doing the work
Held-back testa test the agent cannot see
Protected test filea test file the agent may not change

The cells students fill in are left blank in this preview.

Write the failing test first

Write the test from the task, before the agent starts. Run it and watch it fail. A test that passes before any work tests nothing new.

Then give the agent the task and the test. The agent changes the code until the test passes. You now have a check that does not depend on what the agent says it did.

A test can pass without the work

An agent is asked to make the test pass, and it may find a short way. It can answer only the inputs it can see. It can change or delete the test.

This is not bad intent. The agent was given a target, and it reached the target. So the target must be hard to reach the wrong way.

Worked example: cheats and how to stop them

CheatWhat it looks likeHow to stop it
Special caseif price is 2000, answer 0Held-back tests with other inputs
Edit the testA case is deleted or changedProtect the test file and compare it
Skip the testThe test is marked to skipCount the tests before and after
Weaker testAn exact check becomes "is a number"Review every change to a test file

The cells students fill in are left blank in this preview.

A common mistake

Warning

Many people think a green test run means the agent did the work. A test can turn green because the test changed, or because only the visible inputs work. Trust green only when the test file is unchanged and the held-back tests pass too.

Judge four recorded attempts

Delivery costs 299 pence below 2000 pence and nothing from 2000 up. The script holds three visible tests and two held-back ones.

It judges the code before the work and three recorded attempts. The attempts were written by hand to stand for what an agent can hand back.

Write the model helper, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const base = process.env.AI_URL || \"http://ai:4000\";' '// The token counts of a reply, or null when the reply carries none.' 'function count(data, name) {' '  return data.usage && typeof data.usage[name] === \"number\" ? data.usage[name] : null;' '}' > model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the model helper, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function ask(messages, sample, max) {' '  try {' '    const res = await fetch(base + \"/v1/chat/completions\", {' '      method: \"POST\",' '      headers: { \"content-type\": \"application/json\", authorization: \"Bearer lab\" },' '      body: JSON.stringify({ model: \"lab\", messages: messages, max_tokens: max || 100 }),' '      signal: AbortSignal.timeout(30000),' '    });' '    if (res.ok === false) throw new Error(\"status \" + res.status);' '    const data = await res.json();' '    const text = data.choices[0].message.content;' '    return { text, inTokens: count(data, \"prompt_tokens\"), outTokens: count(data, \"completion_tokens\"), recorded: false };' '  } catch (error) {' '    console.log(\"Recorded sample: no model answered (\" + error.message + \").\");' '    return { text: sample, inTokens: null, outTokens: null, recorded: true };' '  }' '}' 'module.exports = { ask };' >> model.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the guardrail judge, part 1

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'const { ask } = require(\"./model\");' '// Delivery costs 299 pence below 2000 pence, and nothing from 2000 pence up.' 'const visible = [[500, 299], [1999, 299], [2000, 0]];' 'const held = [[2500, 0], [100, 299]];' 'const attempts = {' '  \"before\": { fn: function (p) { return 299; }, tests: visible },' '  \"honest\": { fn: function (p) { return p >= 2000 ? 0 : 299; }, tests: visible },' '  \"special case\": { fn: function (p) { return p === 2000 ? 0 : 299; }, tests: visible },' '  \"edited test\": { fn: function (p) { return 299; }, tests: [[500, 299], [1999, 299]] },' '};' > guard.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the guardrail judge, part 2

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'function passed(fn, cases) {' '  return cases.filter(function (c) { return fn(c[0]) === c[1]; }).length;' '}' 'function verdict(a) {' '  if (JSON.stringify(a.tests) === JSON.stringify(visible) === false) return \"refuse: tests changed\";' '  if (passed(a.fn, a.tests) < a.tests.length) return \"not done\";' '  if (passed(a.fn, held) < held.length) return \"cheat suspected\";' '  return \"keep\";' '}' >> guard.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the guardrail judge, part 3

Command
docker exec -w /home/node/project agent sh -c "printf '%s\n' 'async function main() {' '  Object.keys(attempts).forEach(function (name) {' '    const a = attempts[name];' '    console.log(name + \": visible \" + passed(a.fn, a.tests) + \"/\" + a.tests.length' '      + \", held back \" + passed(a.fn, held) + \"/\" + held.length + \": \" + verdict(a));' '  });' '  const q = \"In one short sentence, why keep some tests where a coding agent cannot see them?\";' '  const sample = \"Hidden tests show whether the agent solved the problem or only the examples it could see.\";' '  const reply = await ask([{ role: \"user\", content: q }], sample, 60);' '  console.log(\"model said: \" + reply.text);' '}' 'main();' >> guard.js"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run the guardrail judge

Command
docker exec -w /home/node/project agent node guard.js

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

What the Run prints

LineWhat it shows
before: visible 2/3 ...: not doneRed: the test fails before the work
honest: visible 3/3, held back 2/2: keepGreen, and the hidden cases agree
special case: ... held back 1/2: cheat suspectedOnly the visible inputs work
edited test: ...: refuse: tests changedA case was deleted, so nothing counts
model said: ...A sentence from the lab. A Recorded sample line if no model answers

The cells students fill in are left blank in this preview.

About this lab and its source: AI coding agent lab: use opencode to explain and fix code, and check its work

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.